Dapr (Distributed Application Runtime) is a runtime that helps you build resilient stateless, and stateful microservices. This sample shows how to deploy a Dapr application to Azure Container Apps using Terraform modules with the Azure Provider and AzAPI Provider Terraform Providers instead of an Azure Resource Manager (ARM) or Bicep template like in the original sample Tutorial: Deploy a Dapr application to Azure Container Apps with an Azure Resource Manager or Bicep .... You can find the code of this sample, along with Terraform modules, under this Azure Sample on GitHub.
In this article and the companion sample, you will learn how to:
With Azure Container Apps, you get a fully managed version of the Dapr APIs when building microservices. When you use Dapr in Azure Container Apps, you can enable sidecars to run next to your microservices, providing a rich set of capabilities. Available Dapr APIs include Service to Service calls, Pub/Sub, Event Bindings, State Stores, and Actors.
In this sample, you deploy the same applications from the Dapr Hello World quickstart.
The application consists of the following:
The following architecture diagram illustrates the components that make up this tutorial:
The Azure Provider can configure Microsoft Azure infrastructure using the Azure Resource Manager API. See the documentation for more information on the data sources and resources supported by the Azure Provider. To learn the basics of Terraform using this provider, follow the hands-on get-started tutorials. If you are interested in the Azure Provider's latest features, see the changelog for version information and release notes.
The AzAPI Provider is a thin layer on top of the Azure ARM REST APIs. This provider compliments the AzureRM provider by enabling the management of Azure resources that are not yet or may never be supported in the AzureRM provider, such as private/public preview services and features. The AzAPI provider enables you to manage any Azure resource type using any API version. This provider complements the AzureRM provider by allowing the management of new Azure resources and properties (including private preview). For more information, see Overview of the Terraform AzAPI provider.
This sample contains Terraform modules to create the following resources:
The following table contains the code of the modules/contains_apps/main.tf
Terraform module used to create the Azure Container Apps environment, Dapr components, and Container Apps.
terraform {
required_version = ">= 1.3"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.43.0"
}
azapi = {
source = "azure/azapi"
}
}
}
resource "azurerm_container_app_environment" "managed_environment" {
name = var.managed_environment_name
location = var.location
resource_group_name = var.resource_group_name
log_analytics_workspace_id = var.workspace_id
infrastructure_subnet_id = var.infrastructure_subnet_id
internal_load_balancer_enabled = var.internal_load_balancer_enabled
tags = var.tags
lifecycle {
ignore_changes = [
tags
]
}
}
resource "azurerm_container_app_environment_dapr_component" "dapr_component" {
for_each = {for component in var.dapr_components: component.name => component}
name = each.key
container_app_environment_id = azurerm_container_app_environment.managed_environment.id
component_type = each.value.component_type
version = each.value.version
ignore_errors = each.value.ignore_errors
init_timeout = each.value.init_timeout
scopes = each.value.scopes
dynamic "metadata" {
for_each = each.value.metadata != null ? each.value.metadata : []
content {
name = metadata.value.name
secret_name = try(metadata.value.secret_name, null)
value = try(metadata.value.value, null)
}
}
dynamic "secret" {
for_each = each.value.secret != null ? each.value.secret : []
content {
name = secret.value.name
value = secret.value.value
}
}
}
resource "azurerm_container_app" "container_app" {
for_each = {for app in var.container_apps: app.name => app}
name = each.key
resource_group_name = var.resource_group_name
container_app_environment_id = azurerm_container_app_environment.managed_environment.id
tags = var.tags
revision_mode = each.value.revision_mode
template {
dynamic "container" {
for_each = coalesce(each.value.template.containers, [])
content {
name = container.value.name
image = container.value.image
args = try(container.value.args, null)
command = try(container.value.command, null)
cpu = container.value.cpu
memory = container.value.memory
dynamic "env" {
for_each = coalesce(container.value.env, [])
content {
name = env.value.name
secret_name = try(env.value.secret_name, null)
value = try(env.value.value, null)
}
}
}
}
min_replicas = try(each.value.template.min_replicas, null)
max_replicas = try(each.value.template.max_replicas, null)
revision_suffix = try(each.value.template.revision_suffix, null)
dynamic "volume" {
for_each = each.value.template.volume != null ? [each.value.template.volume] : []
content {
name = volume.value.name
storage_name = try(volume.value.storage_name, null)
storage_type = try(volume.value.storage_type, null)
}
}
}
dynamic "ingress" {
for_each = each.value.ingress != null ? [each.value.ingress] : []
content {
allow_insecure_connections = try(ingress.value.allow_insecure_connections, null)
external_enabled = try(ingress.value.external_enabled, null)
target_port = ingress.value.target_port
transport = ingress.value.transport
dynamic "traffic_weight" {
for_each = coalesce(ingress.value.traffic_weight, [])
content {
label = traffic_weight.value.label
latest_revision = traffic_weight.value.latest_revision
revision_suffix = traffic_weight.value.revision_suffix
percentage = traffic_weight.value.percentage
}
}
}
}
dynamic "dapr" {
for_each = each.value.dapr != null ? [each.value.dapr] : []
content {
app_id = dapr.value.app_id
app_port = dapr.value.app_port
app_protocol = dapr.value.app_protocol
}
}
dynamic "secret" {
for_each = each.value.secrets != null ? [each.value.secrets] : []
content {
name = secret.value.name
value = secret.value.value
}
}
lifecycle {
ignore_changes = [
tags
]
}
}
resource "azapi_update_resource" "containerapp" {
type = "Microsoft.App/containerApps@2022-10-01"
resource_id = azurerm_container_app.container_app["pythonapp"].id
body = jsonencode({
properties = {
configuration = {
dapr = {
appPort = null
}
}
}
})
depends_on = [
azurerm_container_app.container_app["pythonapp"],
]
}
As you can see, the module uses the following resources of the Azure Provider:
dapr_components
variable. This sample deploys a single State Management Dapr component that uses Azure Blob Storage as a state store.container_apps
variable.When the Azure Provider does not provide the necessary data sources and resources to create Azure resources or the existing data sources and resources do not yet expose a block or property, you can use the data sources and resources of the AzAPI Provider to create or modify Azure resources.
At the time of this writing, the app_port
property under the dapr
block in the azurerm_container_app
resource is defined as required. You should be able to set the value of this property to null
create headless applications, like the pythonapp
in this tutorial, with no ingress, hence, with no app_port
. I submitted a pull request to turn the app_port
property under the dapr
block in the azurerm_container_app
resource from required to optional. While waiting for the pull request to be accepted, as a temporary solution, we can use an azapi_update_resource resource of the AzAPI Provider to set the appPort of the pythonapp
container app to null after creating the resource with the azurerm_container_app of the Azure Provider.
The azapi
folder of the companion project contains an old version of the sample where the Container App environment, Container Apps, and Dapr component used by the sample are all deployed using azapi_resource resources of the AzAPI Provider. Below you can see the code of the azapi/modules/container_apps/main.tf
module.
terraform {
required_version = ">= 1.3"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.43.0"
}
azapi = {
source = "Azure/azapi"
}
}
experiments = [module_variable_optional_attrs]
}
locals {
module_tag = {
"module" = basename(abspath(path.module))
}
tags = merge(var.tags, local.module_tag)
}
resource "azapi_resource" "managed_environment" {
name = var.managed_environment_name
location = var.location
parent_id = var.resource_group_id
type = "Microsoft.App/managedEnvironments@2022-03-01"
tags = local.tags
body = jsonencode({
properties = {
daprAIInstrumentationKey = var.instrumentation_key
appLogsConfiguration = {
destination = "log-analytics"
logAnalyticsConfiguration = {
customerId = var.workspace_id
sharedKey = var.primary_shared_key
}
}
}
})
lifecycle {
ignore_changes = [
tags
]
}
}
resource "azapi_resource" "daprComponents" {
for_each = {for component in var.dapr_components: component.name => component}
name = each.key
parent_id = azapi_resource.managed_environment.id
type = "Microsoft.App/managedEnvironments/daprComponents@2022-03-01"
body = jsonencode({
properties = {
componentType = each.value.componentType
version = each.value.version
ignoreErrors = each.value.ignoreErrors
initTimeout = each.value.initTimeout
secrets = each.value.secrets
metadata = each.value.metadata
scopes = each.value.scopes
}
})
}
resource "azapi_resource" "container_app" {
for_each = {for app in var.container_apps: app.name => app}
name = each.key
location = var.location
parent_id = var.resource_group_id
type = "Microsoft.App/containerApps@2022-03-01"
tags = local.tags
body = jsonencode({
properties: {
managedEnvironmentId = azapi_resource.managed_environment.id
configuration = {
ingress = try(each.value.configuration.ingress, null)
dapr = try(each.value.configuration.dapr, null)
}
template = each.value.template
}
})
lifecycle {
ignore_changes = [
tags
]
}
}
You can use an azapi_resource resource to create any Azure resource. For more information, see Overview of the Terraform AzAPI provider.
All the resources deployed by the modules share the same name prefix. Make sure to configure a name prefix by setting a value for the resource_prefix
variable defined in the variables.tf
file. If you set the value of the resource_prefix
variable to an empty string, the main.tf
module will use a random_string
resource to automatically create a name prefix for the Azure resources. You can use the deploy.sh
bash script to deploy the sample:
#!/bin/bash
# Terraform Init
terraform init
# Terraform validate
terraform validate -compact-warnings
# Terraform plan
terraform plan -compact-warnings -out main.tfplan
# Terraform apply
terraform apply -compact-warnings -auto-approve main.tfplan
This command deploys the Terraform modules that create the following resources:
nodeapp
app server running on targetPort: 3000
with dapr enabled and configured using: "appId": "nodeapp"
and "appPort": 3000
.daprComponents
object of "type": "state.azure.blobstorage"
scoped for use by the nodeapp
for storing state.pythonapp
with no ingress and Dapr enabled that calls the nodeapp
service via dapr service-to-service communication.Viewing your Azure Storage account data can confirm that the services are working correctly.
state
container.order
in the container.Data logged via a container app are stored in the ContainerAppConsoleLogs_CL
custom table in the Log Analytics workspace. You can view logs through the Azure portal or from the command line. Wait a few minutes for the analytics to arrive for the first time before you query the logged data.
ContainerAppConsoleLogs_CL
| project TimeGenerated, ContainerAppName_s, Log_s
| order by TimeGenerated desc
The following image shows the type of response to expect from the command.
Once done, run the following command to delete your resource group and all the resources you created in this tutorial.
az group delete --resource-group $RESOURCE_GROUP
Since pythonapp
continuously makes calls to nodeapp
with messages that get persisted into your configured state store, it is important to complete these cleanup steps to avoid ongoing billable operations.
Please comment below or submit an issue or a PR on GitHub if you have feedback. If you found this article and companion helpful sample, please like the article below and give a star to the project on GitHub, thanks!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.