08 Multi-Container & Dapr
Goal
Enable Dapr on the Container Apps environment to provide service-to-service invocation between the frontend and backend, and add a Redis state store component for persisting patient triage data.
Estimated time
20 minutes.
Official references
- Dapr integration with Container Apps
- Dapr service invocation
- Dapr state store component
- Container Apps as Dapr component backends
Key concepts
| Concept | Purpose |
|---|---|
| Dapr | Distributed Application Runtime — sidecar-based building blocks. |
| Service invocation | Call other services by name without knowing their URL. |
| State store | Persist key-value data via a pluggable component (Redis, Cosmos DB). |
| Component | A Dapr configuration that binds to an external resource. |
Architecture
graph LR
FE[Frontend] -->|Dapr invoke| DS1[Dapr Sidecar]
DS1 -->|service invocation| DS2[Dapr Sidecar]
DS2 --> BE[Backend]
BE --> DS2
DS2 -->|state store| R[(Redis Container)]
Exercise
Step 1 — Deploy a Redis container
Instead of provisioning a managed Azure Cache for Redis (which takes 15+ minutes), deploy Redis as a Container App in the same environment. This is instant and keeps everything self-contained:
source .env
az containerapp create \
--name redis \
--resource-group $RESOURCE_GROUP \
--environment $CONTAINERAPPS_ENVIRONMENT \
--image docker.io/redis:7-alpine \
--target-port 6379 \
--ingress internal \
--min-replicas 1 \
--max-replicas 1 \
--cpu 0.25 \
--memory 0.5Gi
Get the internal FQDN:
REDIS_HOST=$(az containerapp show \
--name redis \
--resource-group $RESOURCE_GROUP \
--query properties.configuration.ingress.fqdn -o tsv)
echo "Redis FQDN: $REDIS_HOST"
Production: use Azure Cache for Redis
For a workshop, a Redis container is fast and free. In production, use Azure Cache for Redis or Azure Managed Redis for durability, TLS, backups, and an SLA.
The beauty of Dapr is that switching is a config change, not a code
change. Just update the component metadata to point at the managed
service — add enableTLS: "true", swap the host/password, and your
app code stays identical. This same pluggability works with Cosmos DB,
Azure SQL, Service Bus, Event Hubs, and other Azure PaaS services.
Step 2 — Enable Dapr on the backend
az containerapp dapr enable \
--name triage-backend \
--resource-group $RESOURCE_GROUP \
--dapr-app-id triage-backend \
--dapr-app-port 8000 \
--dapr-app-protocol http
Enabling Dapr creates a new revision with a sidecar container. The sidecar
injects the DAPR_HTTP_PORT environment variable — the backend code detects
this and automatically switches from in-memory storage to the Dapr state store.
How the backend integrates with Dapr
The backend code (app/backend/main.py) already includes Dapr state
support. It checks for the DAPR_HTTP_PORT environment variable at
startup — if present, it persists patients to the Dapr state store via
HTTP calls to localhost:3500. If absent (e.g. in our earlier AKS
lessons where Dapr is not configured), it falls back to an in-memory
dictionary.
AKS fully supports Dapr as a managed extension — we simply don't cover it in this workshop.
In a real-world scenario, you would need to modify your application code to call the Dapr HTTP or gRPC API (or use a Dapr SDK). Dapr does not magically intercept your existing storage calls — you must explicitly use its state management API. The benefit is that the backing store (Redis, Cosmos DB, PostgreSQL, etc.) becomes a configuration choice rather than a code change.
Step 3 — Enable Dapr on the frontend
az containerapp dapr enable \
--name triage-frontend \
--resource-group $RESOURCE_GROUP \
--dapr-app-id triage-frontend \
--dapr-app-port 80 \
--dapr-app-protocol http
Step 4 — Create the Redis state store component
Save the component definition to a file and apply it:
cat > /tmp/statestore.yaml <<EOF
componentType: state.redis
version: v1
metadata:
- name: redisHost
value: "${REDIS_HOST}:6379"
- name: enableTLS
value: "false"
scopes:
- triage-backend
EOF
az containerapp env dapr-component set \
--name $CONTAINERAPPS_ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
--dapr-component-name statestore \
--yaml /tmp/statestore.yaml
Note
The Redis container has no password and no TLS — fine for a workshop.
For a managed Azure Cache for Redis you would add enableTLS: "true",
use port 6380, and provide the access key via a Container Apps secret.
Step 5 — Test service invocation
Dapr allows calling other services by app ID instead of a URL. Each sidecar
listens on localhost:3500 and routes requests to the target service via the
mesh. Exec into the frontend and invoke the backend through Dapr:
az containerapp exec \
--name triage-frontend \
--resource-group $RESOURCE_GROUP \
--command "curl -s http://localhost:3500/v1.0/invoke/triage-backend/method/api/health"
You should see the backend's health check JSON response, confirming that Dapr service invocation is working between containers.
Step 6 — Verify state persistence across replicas
In Lesson 07 we saw that in-memory state is lost when requests hit different replicas. Now that the backend uses Dapr state backed by Redis, this is fixed.
Open the frontend UI and submit a patient. Refresh multiple times — the patient data is now always visible regardless of which replica serves the request.
Problem solved
All replicas now read and write to the same Redis-backed state store via Dapr. No more disappearing patients!
Step 7 — Test state via the Dapr API directly
You can also interact with the state store directly via the Dapr API:
Save state:
az containerapp exec \
--name triage-backend \
--resource-group $RESOURCE_GROUP \
--command "curl -s -X POST http://localhost:3500/v1.0/state/statestore -H Content-Type:application/json -d [{\"key\":\"patient-001\",\"value\":{\"name\":\"Jane Doe\",\"urgency\":\"High\"}}]"
Retrieve it:
az containerapp exec \
--name triage-backend \
--resource-group $RESOURCE_GROUP \
--command "curl -s http://localhost:3500/v1.0/state/statestore/patient-001"
Step 8 — List Dapr components
az containerapp env dapr-component list \
--name $CONTAINERAPPS_ENVIRONMENT \
--resource-group $RESOURCE_GROUP \
-o table
What this lab demonstrates
- Enabling Dapr sidecars on Container Apps.
- Service-to-service invocation by app ID.
- Pluggable state management with Redis — fixing the multi-replica inconsistency from Lesson 07.
- Dapr component configuration in Container Apps.
- Benefits: service discovery, retries, and observability — without changing application URLs.
Expected result
Both apps have Dapr sidecars. The frontend can invoke the backend via Dapr service invocation. Patient data persists consistently across multiple backend replicas because the state store is now backed by Redis via Dapr.
Verification
- [ ]
az containerapp show --name triage-backend ... --query properties.configuration.daprshows Dapr enabled. - [ ] Service invocation via
localhost:3500returns the health check. - [ ] Patients submitted via the UI are visible on every refresh (no more disappearing data).
- [ ] State store write and read via the Dapr API returns
{"name":"Jane Doe","urgency":"High"}. - [ ]
az containerapp env dapr-component listshows thestatestorecomponent.