/container-apps-azapi-terraform

This sample shows how to deploy a Dapr application to Azure Container Apps using Terraform with the AzAPI Provider.

Primary LanguageHCLMIT LicenseMIT

page_type languages products name description urlFragment
sample
azurecli
bash
terraform
yaml
json
azure
azure-container-apps
azure-storage
azure-blob-storage
azure-storage-accounts
azure-monitor
azure-log-analytics
azure-application-insights
Deploy a Dapr application to Azure Container Apps with Terraform
This sample shows how to deploy a Dapr application to Azure Container Apps using Terraform modules and the AzAPI Provider.
container-apps-azapi-terraform

Deploy a Dapr application to Azure Container Apps with Terraform

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 template.

In this 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 that provide 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:

  • A client (Python) container app to generate messages.
  • A service (Node) container app to consume and persist those messages in a state store

The following architecture diagram illustrates the components that make up this tutorial:

Architecture

Prerequisites

Terraform Providers

The Azure Provider can be used to configure infrastructure in Microsoft Azure using the Azure Resource Manager API's. For more information on the data sources and resources supported by the Azure Provider, see the documentation. 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 very 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 enabling the management of new Azure resources and properties (including private preview). For more information, see Overview of the Terraform AzAPI provider.

Terraform modules

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:

  • azurerm_container_app_environment: this resource is used to create the Azure Container Apps environment which acts as a secure boundary around the container apps. Container Apps in the same environment are deployed in the same virtual network and write logs to the same Log Analytics workspace.
  • azurerm_container_app_environment_dapr_component: the Distributed Application Runtime ([Dapr][dapr-concepts]) is a set of incrementally adoptable features that simplify the authoring of distributed, microservice-based applications. For example, Dapr provides capabilities for enabling application intercommunication, whether through messaging via pub/sub or reliable and secure service-to-service calls. Once Dapr is enabled for a container app, a secondary process will be created alongside your application code that will enable communication with Dapr via HTTP or gRPC. This component is used to deploy a collection of Dapr components defined in the dapr_components variable. This sample deploys a single State Management Dapr component that uses an Azure Blob Storage as a state store.
  • azurerm_container_app: this resource is used to deploy a configurable collection of Azure Container Apps in the Azure Container Apps environment. The container apps are defined in the 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 to 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 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.

AzAPI 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.tfmodule.

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.

Deploy the sample

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:

  • The Container Apps environment and associated Log Analytics workspace for hosting the hello world Dapr solution.
  • An Application Insights instance for Dapr distributed tracing.
  • The nodeapp app server running on targetPort: 3000 with dapr enabled and configured using: "appId": "nodeapp" and "appPort": 3000.
  • The daprComponents object of "type": "state.azure.blobstorage" scoped for use by the nodeapp for storing state.
  • The headless pythonapp with no ingress and Dapr enabled that calls the nodeapp service via dapr service-to-service communication.

Verify the result

Confirm successful state persistence

You can confirm that the services are working correctly by viewing data in your Azure Storage account.

  1. Open the Azure portal in your browser.
  2. Navigate to your storage account.
  3. Select Containers from the menu on the left side.
  4. Select state.
  5. Verify that you can see the file named order in the container.
  6. Select on the file.
  7. Select the Edit tab.
  8. Select the Refresh button to observe updates.

View Logs

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.

  1. Open the Azure portal in your browser.
  2. Navigate to your log analytics workspace.
  3. Select Logs from the menu on the left side.
  4. Run the following Kusto query.
ContainerAppConsoleLogs_CL 
| project TimeGenerated, ContainerAppName_s, Log_s
| order by TimeGenerated desc

The following images shows the type of response to expect from the command.

Logs

Clean up resources

Once you are done, run the following command to delete your resource group along with 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.

Next steps