Skip to content

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

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

  1. Enabling Dapr sidecars on Container Apps.
  2. Service-to-service invocation by app ID.
  3. Pluggable state management with Redis — fixing the multi-replica inconsistency from Lesson 07.
  4. Dapr component configuration in Container Apps.
  5. 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.dapr shows Dapr enabled.
  • [ ] Service invocation via localhost:3500 returns 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 list shows the statestore component.