├── .github ├── CODE_OF_CONDUCT.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── azapi ├── deploy.sh ├── main.tf ├── modules │ ├── application_insights │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── container_apps │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ ├── log_analytics │ │ ├── main.tf │ │ ├── output.tf │ │ └── variables.tf │ └── storage_account │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf ├── outputs.tf └── variables.tf ├── deploy.sh ├── images ├── azure-container-apps-microservices-dapr.png └── logs.png ├── main.tf ├── modules ├── application_insights │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── container_apps │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── log_analytics │ ├── main.tf │ ├── output.tf │ └── variables.tf ├── private_dns_zone │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── private_endpoint │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── storage_account │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── virtual_network │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── outputs.tf └── variables.tf /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | > Please provide us with the following information: 5 | > --------------------------------------------------------------- 6 | 7 | ### This issue is for a: (mark with an `x`) 8 | ``` 9 | - [ ] bug report -> please search issues before submitting 10 | - [ ] feature request 11 | - [ ] documentation issue or request 12 | - [ ] regression (a behavior that used to work and stopped in a new release) 13 | ``` 14 | 15 | ### Minimal steps to reproduce 16 | > 17 | 18 | ### Any log messages given by the failure 19 | > 20 | 21 | ### Expected/desired behavior 22 | > 23 | 24 | ### OS and Version? 25 | > Windows 7, 8 or 10. Linux (which distribution). macOS (Yosemite? El Capitan? Sierra?) 26 | 27 | ### Versions 28 | > 29 | 30 | ### Mention any other details that might be useful 31 | 32 | > --------------------------------------------------------------- 33 | > Thanks! We'll be in touch soon. 34 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Purpose 2 | 3 | * ... 4 | 5 | ## Does this introduce a breaking change? 6 | 7 | ``` 8 | [ ] Yes 9 | [ ] No 10 | ``` 11 | 12 | ## Pull Request Type 13 | What kind of change does this Pull Request introduce? 14 | 15 | 16 | ``` 17 | [ ] Bugfix 18 | [ ] Feature 19 | [ ] Code style update (formatting, local variables) 20 | [ ] Refactoring (no functional changes, no api changes) 21 | [ ] Documentation content changes 22 | [ ] Other... Please describe: 23 | ``` 24 | 25 | ## How to Test 26 | * Get the code 27 | 28 | ``` 29 | git clone [repo-address] 30 | cd [repo-name] 31 | git checkout [branch-name] 32 | npm install 33 | ``` 34 | 35 | * Test the code 36 | 37 | ``` 38 | ``` 39 | 40 | ## What to Check 41 | Verify that the following are valid 42 | * ... 43 | 44 | ## Other Information 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [project-title] Changelog 2 | 3 | 4 | # x.y.z (yyyy-mm-dd) 5 | 6 | *Features* 7 | * ... 8 | 9 | *Bug Fixes* 10 | * ... 11 | 12 | *Breaking Changes* 13 | * ... 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to [project-title] 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | 15 | - [Code of Conduct](#coc) 16 | - [Issues and Bugs](#issue) 17 | - [Feature Requests](#feature) 18 | - [Submission Guidelines](#submit) 19 | 20 | ## Code of Conduct 21 | Help us keep this project open and inclusive. Please read and follow our [Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 22 | 23 | ## Found an Issue? 24 | If you find a bug in the source code or a mistake in the documentation, you can help us by 25 | [submitting an issue](#submit-issue) to the GitHub Repository. Even better, you can 26 | [submit a Pull Request](#submit-pr) with a fix. 27 | 28 | ## Want a Feature? 29 | You can *request* a new feature by [submitting an issue](#submit-issue) to the GitHub 30 | Repository. If you would like to *implement* a new feature, please submit an issue with 31 | a proposal for your work first, to be sure that we can use it. 32 | 33 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 34 | 35 | ## Submission Guidelines 36 | 37 | ### Submitting an Issue 38 | Before you submit an issue, search the archive, maybe your question was already answered. 39 | 40 | If your issue appears to be a bug, and hasn't been reported, open a new issue. 41 | Help us to maximize the effort we can spend fixing issues and adding new 42 | features, by not reporting duplicate issues. Providing the following information will increase the 43 | chances of your issue being dealt with quickly: 44 | 45 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps 46 | * **Version** - what version is affected (e.g. 0.1.2) 47 | * **Motivation for or Use Case** - explain what are you trying to do and why the current behavior is a bug for you 48 | * **Browsers and Operating System** - is this a problem with all browsers? 49 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps 50 | * **Related Issues** - has a similar issue been reported before? 51 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be 52 | causing the problem (line of code or commit) 53 | 54 | You can file new issues by providing the above information at the corresponding repository's issues link: https://github.com/[organization-name]/[repository-name]/issues/new]. 55 | 56 | ### Submitting a Pull Request (PR) 57 | Before you submit your Pull Request (PR) consider the following guidelines: 58 | 59 | * Search the repository (https://github.com/[organization-name]/[repository-name]/pulls) for an open or closed PR 60 | that relates to your submission. You don't want to duplicate effort. 61 | 62 | * Make your changes in a new git fork: 63 | 64 | * Commit your changes using a descriptive commit message 65 | * Push your fork to GitHub: 66 | * In GitHub, create a pull request 67 | * If we suggest changes then: 68 | * Make the required updates. 69 | * Rebase your fork and force push to your GitHub repository (this will update your Pull Request): 70 | 71 | ```shell 72 | git rebase master -i 73 | git push -f 74 | ``` 75 | 76 | That's it! Thank you for your contribution! 77 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | page_type: sample 3 | languages: 4 | - azurecli 5 | - bash 6 | - terraform 7 | - yaml 8 | - json 9 | products: 10 | - azure 11 | - azure-container-apps 12 | - azure-storage 13 | - azure-blob-storage 14 | - azure-storage-accounts 15 | - azure-monitor 16 | - azure-log-analytics 17 | - azure-application-insights 18 | 19 | name: Deploy a Dapr application to Azure Container Apps with Terraform 20 | description: This sample shows how to deploy a Dapr application to Azure Container Apps using Terraform modules and the AzAPI Provider. 21 | urlFragment: container-apps-azapi-terraform 22 | --- 23 | 24 | # Deploy a Dapr application to Azure Container Apps with Terraform 25 | 26 | [Dapr](https://dapr.io/) (Distributed Application Runtime) is a runtime that helps you build resilient stateless and stateful microservices. This sample shows how to deploy a [Dapr](https://dapr.io/) application to [Azure Container Apps](https://docs.microsoft.com/en-us/azure/container-apps/overview) using Terraform modules with the [Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) and [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) 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](https://docs.microsoft.com/en-us/azure/container-apps/microservices-dapr-azure-resource-manager?tabs=bash&pivots=container-apps-bicep). 27 | 28 | In this sample you will learn how to: 29 | 30 | - Use the following resources from the [Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs): 31 | - [azurerm_container_app](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_app) 32 | - [azurerm_container_app_environment](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_app_environment) 33 | - [azurerm_container_app_environment_certificate](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_app_environment_certificate) 34 | - [azurerm_container_app_environment_dapr_component](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_app_environment_dapr_component) 35 | - [azurerm_container_app_environment_storage](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_app_environment_storage) 36 | - Use Terraform and [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) to deploy or update an Azure resource using the following [data sources](https://www.terraform.io/docs/configuration/data-sources.html) and [resources](https://www.terraform.io/docs/configuration/resources.html) 37 | - resources: 38 | - [azapi_resource](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_resource) 39 | - [azapi_resource_action](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_resource_action) 40 | - [azapi_update_resource](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_update_resource) 41 | - data sources: 42 | - [azapi_resource](https://registry.terraform.io/providers/Azure/azapi/latest/docs/data-sources/azapi_resource) 43 | - [azapi_resource_action](https://registry.terraform.io/providers/Azure/azapi/latest/docs/data-sources/azapi_resource_action) 44 | - Create an Azure Blob Storage for use as a [Dapr](https://dapr.io/) state store 45 | - Deploy an [Azure Container Apps environment](https://docs.microsoft.com/en-us/azure/container-apps/environment) to host one or more Azure Container Apps 46 | - Deploy two [Dapr-enabled](https://docs.microsoft.com/en-us/azure/container-apps/dapr-overview?tabs=bicep1%2Cyaml) Azure Container Apps: one that produces orders and one that consumes orders and stores them 47 | - Verify the interaction between the two microservices. 48 | 49 | With Azure Container Apps, you get a [fully managed version of the Dapr APIs](./dapr-overview.md) when building microservices. When you use [Dapr](https://dapr.io/) 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](https://docs.dapr.io/developing-applications/building-blocks/service-invocation/), [Pub/Sub](https://docs.dapr.io/developing-applications/building-blocks/pubsub/), [Event Bindings](https://docs.dapr.io/developing-applications/building-blocks/bindings/), [State Stores](https://docs.dapr.io/developing-applications/building-blocks/state-management/), and [Actors](https://docs.dapr.io/developing-applications/building-blocks/actors/). 50 | 51 | In this sample, you deploy the same applications from the Dapr [Hello World](https://github.com/dapr/quickstarts/tree/master/tutorials/hello-world) quickstart. 52 | 53 | The application consists of: 54 | 55 | - A client (Python) container app to generate messages. 56 | - A service (Node) container app to consume and persist those messages in a state store 57 | 58 | The following architecture diagram illustrates the components that make up this tutorial: 59 | 60 | ![Architecture](./images/azure-container-apps-microservices-dapr.png) 61 | 62 | ## Prerequisites 63 | 64 | - Install [Azure CLI](/cli/azure/install-azure-cli) 65 | - An Azure account with an active subscription is required. If you don't already have one, you can [create an account for free](https://azure.microsoft.com/free/?WT.mc_id=A261C142F). If you don't have one, create a [free Azure account](https://azure.microsoft.com/free/) before you begin. 66 | - [Visual Studio Code](https://code.visualstudio.com/) installed on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms) along with the [HashiCorp Terraform](h 67 | 68 | ## Terraform Providers 69 | 70 | The [Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) can be used to configure infrastructure in [Microsoft Azure](https://azure.microsoft.com/en-us/) using the Azure Resource Manager API's. For more information on the [data sources](https://www.terraform.io/docs/configuration/data-sources.html) and [resources](https://www.terraform.io/docs/configuration/resources.html) supported by the Azure Provider, see the [documentation](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs). To learn the basics of Terraform using this provider, follow the hands-on [get started tutorials](https://learn.hashicorp.com/tutorials/terraform/infrastructure-as-code?in=terraform/azure-get-started). If you are interested in the [Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs)'s latest features, see the [changelog](https://github.com/hashicorp/terraform-provider-azurerm/blob/main/CHANGELOG.md) for version information and release notes. 71 | 72 | The [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) is a very thin layer on top of the Azure ARM REST APIs. This provider compliments the [AzureRM provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) 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](https://docs.microsoft.com/en-us/azure/developer/terraform/overview-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](https://docs.microsoft.com/en-us/azure/developer/terraform/overview-azapi-provider). 73 | 74 | ## Terraform modules 75 | 76 | This sample contains Terraform modules to create the following resources: 77 | 78 | - [Microsoft.OperationalInsights/workspaces](https://docs.microsoft.com/en-us/azure/templates/microsoft.operationalinsights/workspaces): an [Azure Log Analytics](https://docs.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-workspace-overview) workspace used to collect logs and metrics of the [Azure Container Apps environment](https://docs.microsoft.com/en-us/azure/container-apps/environment). 79 | - [Microsoft.Insights/components](https://docs.microsoft.com/en-us/azure/templates/microsoft.insights/components): an [Azure Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) used by the Azure Container Apps for logging and distributed tracing. 80 | - [Microsoft.Storage/storageAccounts](https://docs.microsoft.com/en-us/azure/templates/microsoft.storage/storageaccounts): this storage account is used to store state of the Dapr component. 81 | - [Microsoft.App/managedEnvironments](https://docs.microsoft.com/en-us/azure/templates/microsoft.app/managedenvironments): an [Azure Container Apps environment](https://docs.microsoft.com/en-us/azure/container-apps/environment) that will host two Azure Container Apps. 82 | - [Microsoft.App/managedEnvironments/daprComponents](https://docs.microsoft.com/en-us/azure/templates/microsoft.app/managedenvironments/daprcomponents): a [state management Dapr component](https://docs.dapr.io/developing-applications/building-blocks/state-management/state-management-overview/) that hosts the orders created by the service application. 83 | - [Microsoft.App/containerApps](https://docs.microsoft.com/en-us/azure/templates/microsoft.app/containerapps): two dapr-enabled Container Apps: [hello-k8s-node](https://hub.docker.com/r/dapriosamples/hello-k8s-node) and [hello-k8s-python](https://hub.docker.com/r/dapriosamples/hello-k8s-python) 84 | 85 | 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. 86 | 87 | ```terraform 88 | terraform { 89 | required_version = ">= 1.3" 90 | required_providers { 91 | azurerm = { 92 | source = "hashicorp/azurerm" 93 | version = "~> 3.43.0" 94 | } 95 | azapi = { 96 | source = "azure/azapi" 97 | } 98 | } 99 | } 100 | 101 | resource "azurerm_container_app_environment" "managed_environment" { 102 | name = var.managed_environment_name 103 | location = var.location 104 | resource_group_name = var.resource_group_name 105 | log_analytics_workspace_id = var.workspace_id 106 | infrastructure_subnet_id = var.infrastructure_subnet_id 107 | internal_load_balancer_enabled = var.internal_load_balancer_enabled 108 | tags = var.tags 109 | 110 | lifecycle { 111 | ignore_changes = [ 112 | tags 113 | ] 114 | } 115 | } 116 | 117 | resource "azurerm_container_app_environment_dapr_component" "dapr_component" { 118 | for_each = {for component in var.dapr_components: component.name => component} 119 | 120 | name = each.key 121 | container_app_environment_id = azurerm_container_app_environment.managed_environment.id 122 | component_type = each.value.component_type 123 | version = each.value.version 124 | ignore_errors = each.value.ignore_errors 125 | init_timeout = each.value.init_timeout 126 | scopes = each.value.scopes 127 | 128 | dynamic "metadata" { 129 | for_each = each.value.metadata != null ? each.value.metadata : [] 130 | content { 131 | name = metadata.value.name 132 | secret_name = try(metadata.value.secret_name, null) 133 | value = try(metadata.value.value, null) 134 | } 135 | } 136 | 137 | dynamic "secret" { 138 | for_each = each.value.secret != null ? each.value.secret : [] 139 | content { 140 | name = secret.value.name 141 | value = secret.value.value 142 | } 143 | } 144 | } 145 | 146 | resource "azurerm_container_app" "container_app" { 147 | for_each = {for app in var.container_apps: app.name => app} 148 | 149 | name = each.key 150 | resource_group_name = var.resource_group_name 151 | container_app_environment_id = azurerm_container_app_environment.managed_environment.id 152 | tags = var.tags 153 | revision_mode = each.value.revision_mode 154 | 155 | template { 156 | dynamic "container" { 157 | for_each = coalesce(each.value.template.containers, []) 158 | content { 159 | name = container.value.name 160 | image = container.value.image 161 | args = try(container.value.args, null) 162 | command = try(container.value.command, null) 163 | cpu = container.value.cpu 164 | memory = container.value.memory 165 | 166 | dynamic "env" { 167 | for_each = coalesce(container.value.env, []) 168 | content { 169 | name = env.value.name 170 | secret_name = try(env.value.secret_name, null) 171 | value = try(env.value.value, null) 172 | } 173 | } 174 | } 175 | } 176 | min_replicas = try(each.value.template.min_replicas, null) 177 | max_replicas = try(each.value.template.max_replicas, null) 178 | revision_suffix = try(each.value.template.revision_suffix, null) 179 | 180 | dynamic "volume" { 181 | for_each = each.value.template.volume != null ? [each.value.template.volume] : [] 182 | content { 183 | name = volume.value.name 184 | storage_name = try(volume.value.storage_name, null) 185 | storage_type = try(volume.value.storage_type, null) 186 | } 187 | } 188 | } 189 | 190 | dynamic "ingress" { 191 | for_each = each.value.ingress != null ? [each.value.ingress] : [] 192 | content { 193 | allow_insecure_connections = try(ingress.value.allow_insecure_connections, null) 194 | external_enabled = try(ingress.value.external_enabled, null) 195 | target_port = ingress.value.target_port 196 | transport = ingress.value.transport 197 | 198 | dynamic "traffic_weight" { 199 | for_each = coalesce(ingress.value.traffic_weight, []) 200 | content { 201 | label = traffic_weight.value.label 202 | latest_revision = traffic_weight.value.latest_revision 203 | revision_suffix = traffic_weight.value.revision_suffix 204 | percentage = traffic_weight.value.percentage 205 | } 206 | } 207 | } 208 | } 209 | 210 | dynamic "dapr" { 211 | for_each = each.value.dapr != null ? [each.value.dapr] : [] 212 | content { 213 | app_id = dapr.value.app_id 214 | app_port = dapr.value.app_port 215 | app_protocol = dapr.value.app_protocol 216 | } 217 | } 218 | 219 | dynamic "secret" { 220 | for_each = each.value.secrets != null ? [each.value.secrets] : [] 221 | content { 222 | name = secret.value.name 223 | value = secret.value.value 224 | } 225 | } 226 | 227 | lifecycle { 228 | ignore_changes = [ 229 | tags 230 | ] 231 | } 232 | } 233 | 234 | resource "azapi_update_resource" "containerapp" { 235 | type = "Microsoft.App/containerApps@2022-10-01" 236 | resource_id = azurerm_container_app.container_app["pythonapp"].id 237 | 238 | body = jsonencode({ 239 | properties = { 240 | configuration = { 241 | dapr = { 242 | appPort = null 243 | } 244 | } 245 | } 246 | }) 247 | 248 | depends_on = [ 249 | azurerm_container_app.container_app["pythonapp"], 250 | ] 251 | } 252 | ``` 253 | 254 | As you can see, the module uses the following resources of the [Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs): 255 | 256 | - [azurerm_container_app_environment](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_app_environment): this resource is used to create the [Azure Container Apps environment](https://learn.microsoft.com/en-us/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. 257 | - [azurerm_container_app_environment_dapr_component](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/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](https://docs.dapr.io/developing-applications/building-blocks/state-management/state-management-overview/) Dapr component that uses an [Azure Blob Storage](https://docs.dapr.io/reference/components-reference/supported-state-stores/setup-azure-blobstorage/) as a state store. 258 | - [azurerm_container_app](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_app): this resource is used to deploy a configurable collection of [Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/overview) in the [Azure Container Apps environment](https://learn.microsoft.com/en-us/azure/container-apps/environment). The container apps are defined in the `container_apps` variable. 259 | 260 | When the [Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) does not provide the necessary [data sources](https://www.terraform.io/docs/configuration/data-sources.html) and [resources](https://www.terraform.io/docs/configuration/resources.html) 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](https://registry.terraform.io/providers/azure/azapi/latest/docs) to create or modify Azure resources. 261 | 262 | 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](https://github.com/hashicorp/terraform-provider-azurerm/pull/20567) 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](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_update_resource) resource of the [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) to set the [appPort](https://learn.microsoft.com/en-us/azure/templates/microsoft.app/containerapps?pivots=deployment-language-terraform) of the `pythonapp` container app to null after creating the resource with the [azurerm_container_app](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/container_app) of the [Azure Provider](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs). 263 | 264 | ## AzAPI Provider 265 | 266 | 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](https://registry.terraform.io/providers/Azure/azapi/latest/docs/resources/azapi_resource) resources of the [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs). Below you can see the code of the `azapi/modules/container_apps/main.tf`module. 267 | 268 | ```terraform 269 | terraform { 270 | required_version = ">= 1.3" 271 | required_providers { 272 | azurerm = { 273 | source = "hashicorp/azurerm" 274 | version = "~> 3.43.0" 275 | } 276 | azapi = { 277 | source = "Azure/azapi" 278 | } 279 | } 280 | experiments = [module_variable_optional_attrs] 281 | } 282 | 283 | locals { 284 | module_tag = { 285 | "module" = basename(abspath(path.module)) 286 | } 287 | tags = merge(var.tags, local.module_tag) 288 | } 289 | 290 | resource "azapi_resource" "managed_environment" { 291 | name = var.managed_environment_name 292 | location = var.location 293 | parent_id = var.resource_group_id 294 | type = "Microsoft.App/managedEnvironments@2022-03-01" 295 | tags = local.tags 296 | 297 | body = jsonencode({ 298 | properties = { 299 | daprAIInstrumentationKey = var.instrumentation_key 300 | appLogsConfiguration = { 301 | destination = "log-analytics" 302 | logAnalyticsConfiguration = { 303 | customerId = var.workspace_id 304 | sharedKey = var.primary_shared_key 305 | } 306 | } 307 | } 308 | }) 309 | 310 | lifecycle { 311 | ignore_changes = [ 312 | tags 313 | ] 314 | } 315 | } 316 | 317 | resource "azapi_resource" "daprComponents" { 318 | for_each = {for component in var.dapr_components: component.name => component} 319 | 320 | name = each.key 321 | parent_id = azapi_resource.managed_environment.id 322 | type = "Microsoft.App/managedEnvironments/daprComponents@2022-03-01" 323 | 324 | body = jsonencode({ 325 | properties = { 326 | componentType = each.value.componentType 327 | version = each.value.version 328 | ignoreErrors = each.value.ignoreErrors 329 | initTimeout = each.value.initTimeout 330 | secrets = each.value.secrets 331 | metadata = each.value.metadata 332 | scopes = each.value.scopes 333 | } 334 | }) 335 | } 336 | 337 | resource "azapi_resource" "container_app" { 338 | for_each = {for app in var.container_apps: app.name => app} 339 | 340 | name = each.key 341 | location = var.location 342 | parent_id = var.resource_group_id 343 | type = "Microsoft.App/containerApps@2022-03-01" 344 | tags = local.tags 345 | 346 | body = jsonencode({ 347 | properties: { 348 | managedEnvironmentId = azapi_resource.managed_environment.id 349 | configuration = { 350 | ingress = try(each.value.configuration.ingress, null) 351 | dapr = try(each.value.configuration.dapr, null) 352 | } 353 | template = each.value.template 354 | } 355 | }) 356 | 357 | lifecycle { 358 | ignore_changes = [ 359 | tags 360 | ] 361 | } 362 | } 363 | ``` 364 | 365 | You can use an [azapi_resource](https://docs.microsoft.com/en-us/azure/developer/terraform/overview-azapi-provider) resource to create any Azure resource. For more information, see [Overview of the Terraform AzAPI provider](https://docs.microsoft.com/en-us/azure/developer/terraform/overview-azapi-provider). 366 | 367 | ## Deploy the sample 368 | 369 | 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: 370 | 371 | ```bash 372 | #!/bin/bash 373 | 374 | # Terraform Init 375 | terraform init 376 | 377 | # Terraform validate 378 | terraform validate -compact-warnings 379 | 380 | # Terraform plan 381 | terraform plan -compact-warnings -out main.tfplan 382 | 383 | # Terraform apply 384 | terraform apply -compact-warnings -auto-approve main.tfplan 385 | ``` 386 | 387 | This command deploys the Terraform modules that create the following resources: 388 | 389 | - The Container Apps environment and associated Log Analytics workspace for hosting the hello world Dapr solution. 390 | - An Application Insights instance for Dapr distributed tracing. 391 | - The `nodeapp` app server running on `targetPort: 3000` with dapr enabled and configured using: `"appId": "nodeapp"` and `"appPort": 3000`. 392 | - The `daprComponents` object of `"type": "state.azure.blobstorage"` scoped for use by the `nodeapp` for storing state. 393 | - The headless `pythonapp` with no ingress and Dapr enabled that calls the `nodeapp` service via dapr service-to-service communication. 394 | 395 | ## Verify the result 396 | 397 | ### Confirm successful state persistence 398 | 399 | You can confirm that the services are working correctly by viewing data in your Azure Storage account. 400 | 401 | 1. Open the [Azure portal](https://portal.azure.com) in your browser. 402 | 1. Navigate to your storage account. 403 | 1. Select **Containers** from the menu on the left side. 404 | 1. Select **state**. 405 | 1. Verify that you can see the file named `order` in the container. 406 | 1. Select on the file. 407 | 1. Select the **Edit** tab. 408 | 1. Select the **Refresh** button to observe updates. 409 | 410 | ### View Logs 411 | 412 | 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. 413 | 414 | 1. Open the [Azure portal](https://portal.azure.com) in your browser. 415 | 1. Navigate to your log analytics workspace. 416 | 1. Select **Logs** from the menu on the left side. 417 | 1. Run the following Kusto query. 418 | 419 | ```kql 420 | ContainerAppConsoleLogs_CL 421 | | project TimeGenerated, ContainerAppName_s, Log_s 422 | | order by TimeGenerated desc 423 | ``` 424 | 425 | The following images shows the type of response to expect from the command. 426 | 427 | ![Logs](./images/logs.png) 428 | 429 | ## Clean up resources 430 | 431 | Once you are done, run the following command to delete your resource group along with all the resources you created in this tutorial. 432 | 433 | ```bash 434 | az group delete \ 435 | --resource-group $RESOURCE_GROUP 436 | ``` 437 | 438 | 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. 439 | 440 | ## Next steps 441 | 442 | - [Azure Container Apps overview](https://docs.microsoft.com/en-us/azure/container-apps/overview) 443 | - [Tutorial: Deploy a Dapr application to Azure Container Apps with an Azure Resource Manager or Bicep template](https://docs.microsoft.com/en-us/azure/container-apps/microservices-dapr-azure-resource-manager?tabs=bash&pivots=container-apps-bicep) 444 | - [AzAPI provider](https://docs.microsoft.com/en-us/azure/developer/terraform/overview-azapi-provider) 445 | - [Announcing Azure Terrafy and AzAPI Terraform Provider Previews](https://techcommunity.microsoft.com/t5/azure-tools-blog/announcing-azure-terrafy-and-azapi-terraform-provider-previews/ba-p/3270937) 446 | -------------------------------------------------------------------------------- /azapi/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Terraform Init 4 | terraform init 5 | 6 | # Terraform validate 7 | terraform validate -compact-warnings 8 | 9 | # Terraform plan 10 | terraform plan -compact-warnings -out main.tfplan 11 | 12 | # Terraform apply 13 | terraform apply -compact-warnings -auto-approve main.tfplan -------------------------------------------------------------------------------- /azapi/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "3.3.0" 7 | } 8 | azapi = { 9 | source = "Azure/azapi" 10 | } 11 | } 12 | experiments = [module_variable_optional_attrs] 13 | } 14 | 15 | provider "azurerm" { 16 | features {} 17 | } 18 | 19 | provider "azapi" { 20 | } 21 | 22 | resource "random_string" "resource_prefix" { 23 | length = 6 24 | special = false 25 | upper = false 26 | numeric = false 27 | } 28 | 29 | resource "azurerm_resource_group" "rg" { 30 | name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.resource_group_name}" 31 | location = var.location 32 | tags = var.tags 33 | } 34 | 35 | module "log_analytics_workspace" { 36 | source = "./modules/log_analytics" 37 | name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.log_analytics_workspace_name}" 38 | location = var.location 39 | resource_group_name = azurerm_resource_group.rg.name 40 | tags = var.tags 41 | } 42 | 43 | module "application_insights" { 44 | source = "./modules/application_insights" 45 | name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.application_insights_name}" 46 | location = var.location 47 | resource_group_name = azurerm_resource_group.rg.name 48 | tags = var.tags 49 | application_type = var.application_insights_application_type 50 | workspace_id = module.log_analytics_workspace.id 51 | } 52 | 53 | module "storage_account" { 54 | source = "./modules/storage_account" 55 | name = lower("${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.storage_account_name}") 56 | location = var.location 57 | resource_group_name = azurerm_resource_group.rg.name 58 | tags = var.tags 59 | account_kind = var.storage_account_kind 60 | account_tier = var.storage_account_tier 61 | replication_type = var.storage_account_replication_type 62 | } 63 | 64 | module "container_app" { 65 | source = "./modules/container_apps" 66 | managed_environment_name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.managed_environment_name}" 67 | location = var.location 68 | resource_group_id = azurerm_resource_group.rg.id 69 | tags = var.tags 70 | instrumentation_key = module.application_insights.instrumentation_key 71 | workspace_id = module.log_analytics_workspace.workspace_id 72 | primary_shared_key = module.log_analytics_workspace.primary_shared_key 73 | dapr_components = [{ 74 | name = var.dapr_component_name 75 | componentType = var.dapr_component_type 76 | version = var.dapr_component_version 77 | ignoreErrors = var.dapr_ignore_errors 78 | initTimeout = var.dapr_component_init_timeout 79 | secrets = [ 80 | { 81 | name = "storageaccountkey" 82 | value = module.storage_account.primary_access_key 83 | } 84 | ] 85 | metadata: [ 86 | { 87 | name = "accountName" 88 | value = module.storage_account.name 89 | }, 90 | { 91 | name = "containerName" 92 | value = var.container_name 93 | }, 94 | { 95 | name = "accountKey" 96 | secretRef = "storageaccountkey" 97 | } 98 | ] 99 | scopes = var.dapr_component_scopes 100 | }] 101 | container_apps = var.container_apps 102 | } -------------------------------------------------------------------------------- /azapi/modules/application_insights/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "3.3.0" 7 | } 8 | azapi = { 9 | source = "Azure/azapi" 10 | version = "0.4.0" 11 | } 12 | } 13 | experiments = [module_variable_optional_attrs] 14 | } 15 | 16 | locals { 17 | module_tag = { 18 | "module" = basename(abspath(path.module)) 19 | } 20 | tags = merge(var.tags, local.module_tag) 21 | } 22 | 23 | resource "azurerm_application_insights" "resource" { 24 | name = var.name 25 | location = var.location 26 | resource_group_name = var.resource_group_name 27 | tags = local.tags 28 | application_type = "web" 29 | workspace_id = var.workspace_id 30 | 31 | lifecycle { 32 | ignore_changes = [ 33 | tags 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /azapi/modules/application_insights/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | value = azurerm_application_insights.resource.name 3 | description = "Specifies the name of the resource." 4 | } 5 | 6 | output "id" { 7 | value = azurerm_application_insights.resource.id 8 | description = "Specifies the resource id of the resource." 9 | } 10 | 11 | output "instrumentation_key" { 12 | value = azurerm_application_insights.resource.instrumentation_key 13 | description = "Specifies the instrumentation key of the Application Insights." 14 | } 15 | 16 | output "app_id" { 17 | value = azurerm_application_insights.resource.app_id 18 | description = "Specifies the resource id of the resource." 19 | } -------------------------------------------------------------------------------- /azapi/modules/application_insights/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "name" { 3 | description = "(Required) Specifies the name of the resource. Changing this forces a new resource to be created." 4 | type = string 5 | } 6 | 7 | variable "resource_group_name" { 8 | description = "(Required) The name of the resource group in which to create the resource. Changing this forces a new resource to be created." 9 | type = string 10 | } 11 | 12 | variable "tags" { 13 | description = "(Optional) Specifies the tags of the log analytics workspace" 14 | type = map(any) 15 | default = {} 16 | } 17 | 18 | variable "location" { 19 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 20 | type = string 21 | } 22 | 23 | variable "application_type" { 24 | description = "(Required) Specifies the type of Application Insights to create. Valid values are ios for iOS, java for Java web, MobileCenter for App Center, Node.JS for Node.js, other for General, phone for Windows Phone, store for Windows Store and web for ASP.NET. Please note these values are case sensitive; unmatched values are treated as ASP.NET by Azure. Changing this forces a new resource to be created." 25 | type = string 26 | default = "web" 27 | } 28 | 29 | variable "workspace_id" { 30 | description = "(Optional) Specifies the id of a log analytics workspace resource. Changing this forces a new resource to be created." 31 | type = string 32 | } 33 | -------------------------------------------------------------------------------- /azapi/modules/container_apps/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "3.3.0" 7 | } 8 | azapi = { 9 | source = "Azure/azapi" 10 | } 11 | } 12 | experiments = [module_variable_optional_attrs] 13 | } 14 | 15 | locals { 16 | module_tag = { 17 | "module" = basename(abspath(path.module)) 18 | } 19 | tags = merge(var.tags, local.module_tag) 20 | } 21 | 22 | resource "azapi_resource" "managed_environment" { 23 | name = var.managed_environment_name 24 | location = var.location 25 | parent_id = var.resource_group_id 26 | type = "Microsoft.App/managedEnvironments@2022-03-01" 27 | tags = local.tags 28 | 29 | body = jsonencode({ 30 | properties = { 31 | daprAIInstrumentationKey = var.instrumentation_key 32 | appLogsConfiguration = { 33 | destination = "log-analytics" 34 | logAnalyticsConfiguration = { 35 | customerId = var.workspace_id 36 | sharedKey = var.primary_shared_key 37 | } 38 | } 39 | } 40 | }) 41 | 42 | lifecycle { 43 | ignore_changes = [ 44 | tags 45 | ] 46 | } 47 | } 48 | 49 | resource "azapi_resource" "daprComponents" { 50 | for_each = {for component in var.dapr_components: component.name => component} 51 | 52 | name = each.key 53 | parent_id = azapi_resource.managed_environment.id 54 | type = "Microsoft.App/managedEnvironments/daprComponents@2022-03-01" 55 | 56 | body = jsonencode({ 57 | properties = { 58 | componentType = each.value.componentType 59 | version = each.value.version 60 | ignoreErrors = each.value.ignoreErrors 61 | initTimeout = each.value.initTimeout 62 | secrets = each.value.secrets 63 | metadata = each.value.metadata 64 | scopes = each.value.scopes 65 | } 66 | }) 67 | } 68 | 69 | resource "azapi_resource" "container_app" { 70 | for_each = {for app in var.container_apps: app.name => app} 71 | 72 | name = each.key 73 | location = var.location 74 | parent_id = var.resource_group_id 75 | type = "Microsoft.App/containerApps@2022-03-01" 76 | tags = local.tags 77 | 78 | body = jsonencode({ 79 | properties: { 80 | managedEnvironmentId = azapi_resource.managed_environment.id 81 | configuration = { 82 | ingress = try(each.value.configuration.ingress, null) 83 | dapr = try(each.value.configuration.dapr, null) 84 | } 85 | template = each.value.template 86 | } 87 | }) 88 | 89 | lifecycle { 90 | ignore_changes = [ 91 | tags 92 | ] 93 | } 94 | } -------------------------------------------------------------------------------- /azapi/modules/container_apps/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | value = azapi_resource.managed_environment.name 3 | description = "Specifies the name of the managed environment." 4 | } 5 | 6 | output "id" { 7 | value = azapi_resource.managed_environment.id 8 | description = "Specifies the resource id of the managed environment." 9 | } -------------------------------------------------------------------------------- /azapi/modules/container_apps/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "managed_environment_name" { 3 | description = "(Required) Specifies the name of the managed environment." 4 | type = string 5 | } 6 | 7 | variable "resource_group_id" { 8 | description = "(Required) The resource id of the resource group in which to create the resource. Changing this forces a new resource to be created." 9 | type = string 10 | } 11 | 12 | variable "tags" { 13 | description = "(Optional) Specifies the tags of the log analytics workspace" 14 | type = map(any) 15 | default = {} 16 | } 17 | 18 | variable "location" { 19 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 20 | type = string 21 | } 22 | 23 | variable "instrumentation_key" { 24 | description = "(Optional) Specifies the instrumentation key of the application insights resource." 25 | type = string 26 | } 27 | 28 | variable "workspace_id" { 29 | description = "(Optional) Specifies workspace id of the log analytics workspace." 30 | type = string 31 | } 32 | 33 | variable "primary_shared_key" { 34 | description = "(Optional) Specifies the workspace key of the log analytics workspace." 35 | type = string 36 | } 37 | 38 | variable "dapr_components" { 39 | description = "Specifies the dapr components in the managed environment." 40 | type = list(object({ 41 | name = string 42 | componentType = string 43 | version = string 44 | ignoreErrors = optional(bool) 45 | initTimeout = string 46 | secrets = optional(list(object({ 47 | name = string 48 | value = any 49 | }))) 50 | metadata = optional(list(object({ 51 | name = string 52 | value = optional(any) 53 | secretRef = optional(any) 54 | }))) 55 | scopes = optional(list(string)) 56 | })) 57 | } 58 | 59 | variable "container_apps" { 60 | description = "Specifies the container apps in the managed environment." 61 | type = list(object({ 62 | name = string 63 | configuration = object({ 64 | ingress = optional(object({ 65 | external = optional(bool) 66 | targetPort = optional(number) 67 | })) 68 | dapr = optional(object({ 69 | enabled = optional(bool) 70 | appId = optional(string) 71 | appProtocol = optional(string) 72 | appPort = optional(number) 73 | })) 74 | }) 75 | template = object({ 76 | containers = list(object({ 77 | image = string 78 | name = string 79 | env = optional(list(object({ 80 | name = string 81 | value = string 82 | }))) 83 | resources = optional(object({ 84 | cpu = optional(number) 85 | memory = optional(string) 86 | })) 87 | })) 88 | scale = optional(object({ 89 | minReplicas = optional(number) 90 | maxReplicas = optional(number) 91 | })) 92 | }) 93 | })) 94 | } -------------------------------------------------------------------------------- /azapi/modules/log_analytics/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "3.3.0" 7 | } 8 | azapi = { 9 | source = "Azure/azapi" 10 | version = "0.4.0" 11 | } 12 | } 13 | experiments = [module_variable_optional_attrs] 14 | } 15 | 16 | locals { 17 | module_tag = { 18 | "module" = basename(abspath(path.module)) 19 | } 20 | tags = merge(var.tags, local.module_tag) 21 | } 22 | 23 | resource "azurerm_log_analytics_workspace" "log_analytics_workspace" { 24 | name = var.name 25 | location = var.location 26 | resource_group_name = var.resource_group_name 27 | sku = var.sku 28 | tags = local.tags 29 | retention_in_days = var.retention_in_days != "" ? var.retention_in_days : null 30 | 31 | lifecycle { 32 | ignore_changes = [ 33 | tags 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /azapi/modules/log_analytics/output.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = azurerm_log_analytics_workspace.log_analytics_workspace.id 3 | description = "Specifies the resource id of the log analytics workspace" 4 | } 5 | 6 | output "location" { 7 | value = azurerm_log_analytics_workspace.log_analytics_workspace.location 8 | description = "Specifies the location of the log analytics workspace" 9 | } 10 | 11 | output "name" { 12 | value = azurerm_log_analytics_workspace.log_analytics_workspace.name 13 | description = "Specifies the name of the log analytics workspace" 14 | } 15 | 16 | output "resource_group_name" { 17 | value = azurerm_log_analytics_workspace.log_analytics_workspace.resource_group_name 18 | description = "Specifies the name of the resource group that contains the log analytics workspace" 19 | } 20 | 21 | output "workspace_id" { 22 | value = azurerm_log_analytics_workspace.log_analytics_workspace.workspace_id 23 | description = "Specifies the workspace id of the log analytics workspace" 24 | } 25 | 26 | output "primary_shared_key" { 27 | value = azurerm_log_analytics_workspace.log_analytics_workspace.primary_shared_key 28 | description = "Specifies the workspace key of the log analytics workspace" 29 | sensitive = true 30 | } 31 | -------------------------------------------------------------------------------- /azapi/modules/log_analytics/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "(Required) Specifies the name of the log analytics workspace" 3 | type = string 4 | } 5 | 6 | variable "resource_group_name" { 7 | description = "(Required) Specifies the resource group name" 8 | type = string 9 | } 10 | 11 | variable "location" { 12 | description = "(Required) Specifies the location of the log analytics workspace" 13 | type = string 14 | } 15 | 16 | variable "sku" { 17 | description = "(Optional) Specifies the sku of the log analytics workspace" 18 | type = string 19 | default = "PerGB2018" 20 | 21 | validation { 22 | condition = contains(["Free", "Standalone", "PerNode", "PerGB2018"], var.sku) 23 | error_message = "The log analytics sku is incorrect." 24 | } 25 | } 26 | 27 | variable "tags" { 28 | description = "(Optional) Specifies the tags of the log analytics workspace" 29 | type = map(any) 30 | default = {} 31 | } 32 | 33 | variable "retention_in_days" { 34 | description = " (Optional) Specifies the workspace data retention in days. Possible values are either 7 (Free Tier only) or range between 30 and 730." 35 | type = number 36 | default = 30 37 | } -------------------------------------------------------------------------------- /azapi/modules/storage_account/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "3.3.0" 7 | } 8 | azapi = { 9 | source = "Azure/azapi" 10 | version = "0.4.0" 11 | } 12 | } 13 | experiments = [module_variable_optional_attrs] 14 | } 15 | 16 | resource "azurerm_storage_account" "storage_account" { 17 | name = var.name 18 | resource_group_name = var.resource_group_name 19 | 20 | location = var.location 21 | account_kind = var.account_kind 22 | account_tier = var.account_tier 23 | account_replication_type = var.replication_type 24 | is_hns_enabled = var.is_hns_enabled 25 | tags = var.tags 26 | 27 | network_rules { 28 | default_action = (length(var.ip_rules) + length(var.virtual_network_subnet_ids)) > 0 ? "Deny" : var.default_action 29 | ip_rules = var.ip_rules 30 | virtual_network_subnet_ids = var.virtual_network_subnet_ids 31 | } 32 | 33 | identity { 34 | type = "SystemAssigned" 35 | } 36 | 37 | lifecycle { 38 | ignore_changes = [ 39 | tags 40 | ] 41 | } 42 | } -------------------------------------------------------------------------------- /azapi/modules/storage_account/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | description = "Specifies the name of the storage account." 3 | value = azurerm_storage_account.storage_account.name 4 | } 5 | 6 | output "id" { 7 | description = "Specifies the resource id of the storage account." 8 | value = azurerm_storage_account.storage_account.id 9 | } 10 | 11 | output "primary_access_key" { 12 | description = "Specifies the primary access key of the storage account." 13 | value = azurerm_storage_account.storage_account.primary_access_key 14 | } 15 | 16 | output "principal_id" { 17 | description = "Specifies the principal id of the system assigned managed identity of the storage account." 18 | value = azurerm_storage_account.storage_account.identity[0].principal_id 19 | } 20 | 21 | output "primary_blob_endpoint" { 22 | description = "Specifies the primary blob endpoint of the storage account." 23 | value = azurerm_storage_account.storage_account.primary_blob_endpoint 24 | } -------------------------------------------------------------------------------- /azapi/modules/storage_account/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | description = "(Required) Specifies the resource group name of the storage account" 3 | type = string 4 | } 5 | 6 | variable "name" { 7 | description = "(Required) Specifies the name of the storage account" 8 | type = string 9 | } 10 | 11 | variable "location" { 12 | description = "(Required) Specifies the location of the storage account" 13 | type = string 14 | } 15 | 16 | variable "account_kind" { 17 | description = "(Optional) Specifies the account kind of the storage account" 18 | default = "StorageV2" 19 | type = string 20 | 21 | validation { 22 | condition = contains(["Storage", "StorageV2"], var.account_kind) 23 | error_message = "The account kind of the storage account is invalid." 24 | } 25 | } 26 | 27 | variable "account_tier" { 28 | description = "(Optional) Specifies the account tier of the storage account" 29 | default = "Standard" 30 | type = string 31 | 32 | validation { 33 | condition = contains(["Standard", "Premium"], var.account_tier) 34 | error_message = "The account tier of the storage account is invalid." 35 | } 36 | } 37 | 38 | variable "replication_type" { 39 | description = "(Optional) Specifies the replication type of the storage account" 40 | default = "LRS" 41 | type = string 42 | 43 | validation { 44 | condition = contains(["LRS", "ZRS", "GRS", "GZRS", "RA-GRS", "RA-GZRS"], var.replication_type) 45 | error_message = "The replication type of the storage account is invalid." 46 | } 47 | } 48 | 49 | variable "is_hns_enabled" { 50 | description = "(Optional) Specifies the replication type of the storage account" 51 | default = false 52 | type = bool 53 | } 54 | 55 | variable "default_action" { 56 | description = "Allow or disallow public access to all blobs or containers in the storage accounts. The default interpretation is true for this property." 57 | default = "Allow" 58 | type = string 59 | } 60 | 61 | variable "ip_rules" { 62 | description = "Specifies IP rules for the storage account" 63 | default = [] 64 | type = list(string) 65 | } 66 | 67 | variable "virtual_network_subnet_ids" { 68 | description = "Specifies a list of resource ids for subnets" 69 | default = [] 70 | type = list(string) 71 | } 72 | 73 | variable "kind" { 74 | description = "(Optional) Specifies the kind of the storage account" 75 | default = "" 76 | } 77 | 78 | variable "tags" { 79 | description = "(Optional) Specifies the tags of the storage account" 80 | default = {} 81 | } -------------------------------------------------------------------------------- /azapi/outputs.tf: -------------------------------------------------------------------------------- 1 | output "log_analytics_name" { 2 | value = module.log_analytics_workspace.name 3 | } 4 | 5 | output "log_analytics_workspace_id" { 6 | value = module.log_analytics_workspace.workspace_id 7 | } 8 | -------------------------------------------------------------------------------- /azapi/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_prefix" { 2 | description = "Specifies a prefix for all the resource names." 3 | default = "Astra" 4 | type = string 5 | } 6 | 7 | variable "location" { 8 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 9 | type = string 10 | default = "WestEurope" 11 | } 12 | 13 | variable "resource_group_name" { 14 | description = "Name of the resource group in which the resources will be created" 15 | default = "RG" 16 | } 17 | 18 | variable "tags" { 19 | description = "(Optional) Specifies tags for all the resources" 20 | default = { 21 | createdWith = "Terraform" 22 | } 23 | } 24 | 25 | variable "log_analytics_workspace_name" { 26 | description = "Specifies the name of the log analytics workspace" 27 | default = "Workspace" 28 | type = string 29 | } 30 | 31 | variable "log_analytics_retention_days" { 32 | description = "Specifies the number of days of the retention policy for the log analytics workspace." 33 | type = number 34 | default = 30 35 | } 36 | 37 | variable "application_insights_name" { 38 | description = "Specifies the name of the application insights resource." 39 | default = "ApplicationInsights" 40 | type = string 41 | } 42 | 43 | variable "application_insights_application_type" { 44 | description = "(Required) Specifies the type of Application Insights to create. Valid values are ios for iOS, java for Java web, MobileCenter for App Center, Node.JS for Node.js, other for General, phone for Windows Phone, store for Windows Store and web for ASP.NET. Please note these values are case sensitive; unmatched values are treated as ASP.NET by Azure. Changing this forces a new resource to be created." 45 | type = string 46 | default = "web" 47 | } 48 | 49 | variable "storage_account_name" { 50 | description = "(Optional) Specifies the name of the storage account" 51 | default = "account" 52 | type = string 53 | } 54 | 55 | variable "storage_account_replication_type" { 56 | description = "(Optional) Specifies the replication type of the storage account" 57 | default = "LRS" 58 | type = string 59 | 60 | validation { 61 | condition = contains(["LRS", "ZRS", "GRS", "GZRS", "RA-GRS", "RA-GZRS"], var.storage_account_replication_type) 62 | error_message = "The replication type of the storage account is invalid." 63 | } 64 | } 65 | 66 | variable "storage_account_kind" { 67 | description = "(Optional) Specifies the account kind of the storage account" 68 | default = "StorageV2" 69 | type = string 70 | 71 | validation { 72 | condition = contains(["Storage", "StorageV2"], var.storage_account_kind) 73 | error_message = "The account kind of the storage account is invalid." 74 | } 75 | } 76 | 77 | variable "storage_account_tier" { 78 | description = "(Optional) Specifies the account tier of the storage account" 79 | default = "Standard" 80 | type = string 81 | 82 | validation { 83 | condition = contains(["Standard", "Premium"], var.storage_account_tier) 84 | error_message = "The account tier of the storage account is invalid." 85 | } 86 | } 87 | 88 | variable "managed_environment_name" { 89 | description = "(Required) Specifies the name of the managed environment." 90 | type = string 91 | default = "ManagedEnvironment" 92 | } 93 | 94 | variable "dapr_component_name" { 95 | description = "(Required) Specifies the name of the dapr component." 96 | type = string 97 | default = "statestore" 98 | } 99 | 100 | variable "dapr_component_type" { 101 | description = "(Required) Specifies the type of the dapr component." 102 | type = string 103 | default = "state.azure.blobstorage" 104 | } 105 | 106 | variable "dapr_ignore_errors" { 107 | description = "(Required) Specifies if the component errors are ignored." 108 | type = bool 109 | default = false 110 | } 111 | 112 | variable "dapr_component_version" { 113 | description = "(Required) Specifies the version of the dapr component." 114 | type = string 115 | default = "v1" 116 | } 117 | 118 | variable "dapr_component_init_timeout" { 119 | description = "(Required) Specifies the init timeout of the dapr component." 120 | type = string 121 | default = "5s" 122 | } 123 | 124 | variable "dapr_component_scopes" { 125 | description = "(Required) Specifies the init timeout of the dapr component." 126 | type = list 127 | default = ["nodeapp"] 128 | } 129 | 130 | variable "container_name" { 131 | description = "Specifies the name of the container in the storage account." 132 | type = string 133 | default = "state" 134 | } 135 | 136 | variable "container_access_type" { 137 | description = "Specifies the access type of the container in the storage account." 138 | type = string 139 | default = "private" 140 | } 141 | 142 | variable "container_apps" { 143 | description = "Specifies the container apps in the managed environment." 144 | type = list(object({ 145 | name = string 146 | configuration = object({ 147 | ingress = optional(object({ 148 | external = optional(bool) 149 | targetPort = optional(number) 150 | })) 151 | dapr = optional(object({ 152 | enabled = optional(bool) 153 | appId = optional(string) 154 | appProtocol = optional(string) 155 | appPort = optional(number) 156 | })) 157 | }) 158 | template = object({ 159 | containers = list(object({ 160 | image = string 161 | name = string 162 | env = optional(list(object({ 163 | name = string 164 | value = string 165 | }))) 166 | resources = optional(object({ 167 | cpu = optional(number) 168 | memory = optional(string) 169 | })) 170 | })) 171 | scale = optional(object({ 172 | minReplicas = optional(number) 173 | maxReplicas = optional(number) 174 | })) 175 | }) 176 | })) 177 | default = [{ 178 | name = "nodeapp" 179 | configuration = { 180 | ingress = { 181 | external = false 182 | targetPort = 3000 183 | } 184 | dapr = { 185 | enabled = true 186 | appId = "nodeapp" 187 | appProtocol = "http" 188 | appPort = 3000 189 | } 190 | } 191 | template = { 192 | containers = [{ 193 | image = "dapriosamples/hello-k8s-node:latest" 194 | name = "hello-k8s-node" 195 | env = [{ 196 | name = "APP_PORT" 197 | value = 3000 198 | }] 199 | resources = { 200 | cpu = 0.5 201 | memory = "1.0Gi" 202 | } 203 | }] 204 | scale = { 205 | minReplicas = 1 206 | maxReplicas = 1 207 | } 208 | } 209 | }, 210 | { 211 | name = "pythonapp" 212 | configuration = { 213 | dapr = { 214 | enabled = true 215 | appId = "pythonapp" 216 | } 217 | } 218 | template = { 219 | containers = [{ 220 | image = "dapriosamples/hello-k8s-python:latest" 221 | name = "hello-k8s-python" 222 | resources = { 223 | cpu = 0.5 224 | memory = "1.0Gi" 225 | } 226 | }] 227 | scale = { 228 | minReplicas = 1 229 | maxReplicas = 1 230 | } 231 | } 232 | }] 233 | } -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Terraform Init 4 | terraform init 5 | 6 | # Terraform validate 7 | terraform validate -compact-warnings 8 | 9 | # Terraform plan 10 | terraform plan -compact-warnings -out main.tfplan 11 | 12 | # Terraform apply 13 | terraform apply -compact-warnings -auto-approve main.tfplan -------------------------------------------------------------------------------- /images/azure-container-apps-microservices-dapr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-azapi-terraform/6a44c7e8e95afbe2a5880597c9a79589c3a9c045/images/azure-container-apps-microservices-dapr.png -------------------------------------------------------------------------------- /images/logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Azure-Samples/container-apps-azapi-terraform/6a44c7e8e95afbe2a5880597c9a79589c3a9c045/images/logs.png -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.43.0" 7 | } 8 | } 9 | } 10 | 11 | provider "azurerm" { 12 | features {} 13 | } 14 | 15 | data "azurerm_client_config" "current" { 16 | } 17 | 18 | resource "random_string" "resource_prefix" { 19 | length = 6 20 | special = false 21 | upper = false 22 | numeric = false 23 | } 24 | 25 | resource "azurerm_resource_group" "rg" { 26 | name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.resource_group_name}" 27 | location = var.location 28 | tags = var.tags 29 | } 30 | 31 | module "log_analytics_workspace" { 32 | source = "./modules/log_analytics" 33 | name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.log_analytics_workspace_name}" 34 | location = var.location 35 | resource_group_name = azurerm_resource_group.rg.name 36 | tags = var.tags 37 | } 38 | 39 | module "application_insights" { 40 | source = "./modules/application_insights" 41 | name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.application_insights_name}" 42 | location = var.location 43 | resource_group_name = azurerm_resource_group.rg.name 44 | tags = var.tags 45 | application_type = var.application_insights_application_type 46 | workspace_id = module.log_analytics_workspace.id 47 | } 48 | 49 | module "virtual_network" { 50 | source = "./modules/virtual_network" 51 | resource_group_name = azurerm_resource_group.rg.name 52 | vnet_name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.vnet_name}" 53 | location = var.location 54 | address_space = var.vnet_address_space 55 | tags = var.tags 56 | log_analytics_workspace_id = module.log_analytics_workspace.id 57 | log_analytics_retention_days = var.log_analytics_retention_days 58 | 59 | subnets = [ 60 | { 61 | name : var.aca_subnet_name 62 | address_prefixes : var.aca_subnet_address_prefix 63 | private_endpoint_network_policies_enabled : true 64 | private_link_service_network_policies_enabled : false 65 | }, 66 | { 67 | name : var.private_endpoint_subnet_name 68 | address_prefixes : var.private_endpoint_subnet_address_prefix 69 | private_endpoint_network_policies_enabled : true 70 | private_link_service_network_policies_enabled : false 71 | } 72 | ] 73 | } 74 | 75 | module "blob_private_dns_zone" { 76 | source = "./modules/private_dns_zone" 77 | name = "privatelink.blob.core.windows.net" 78 | resource_group_name = azurerm_resource_group.rg.name 79 | virtual_networks_to_link = { 80 | (module.virtual_network.name) = { 81 | subscription_id = data.azurerm_client_config.current.subscription_id 82 | resource_group_name = azurerm_resource_group.rg.name 83 | } 84 | } 85 | } 86 | 87 | module "blob_private_endpoint" { 88 | source = "./modules/private_endpoint" 89 | name = "${title(module.storage_account.name)}PrivateEndpoint" 90 | location = var.location 91 | resource_group_name = azurerm_resource_group.rg.name 92 | subnet_id = module.virtual_network.subnet_ids[var.private_endpoint_subnet_name] 93 | tags = var.tags 94 | private_connection_resource_id = module.storage_account.id 95 | is_manual_connection = false 96 | subresource_name = "blob" 97 | private_dns_zone_group_name = "BlobPrivateDnsZoneGroup" 98 | private_dns_zone_group_ids = [module.blob_private_dns_zone.id] 99 | } 100 | 101 | module "storage_account" { 102 | source = "./modules/storage_account" 103 | name = lower("${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.storage_account_name}") 104 | location = var.location 105 | resource_group_name = azurerm_resource_group.rg.name 106 | tags = var.tags 107 | account_kind = var.storage_account_kind 108 | account_tier = var.storage_account_tier 109 | replication_type = var.storage_account_replication_type 110 | } 111 | 112 | module "container_apps" { 113 | source = "./modules/container_apps" 114 | managed_environment_name = "${var.resource_prefix != "" ? var.resource_prefix : random_string.resource_prefix.result}${var.managed_environment_name}" 115 | location = var.location 116 | resource_group_name = azurerm_resource_group.rg.name 117 | tags = var.tags 118 | infrastructure_subnet_id = module.virtual_network.subnet_ids[var.aca_subnet_name] 119 | instrumentation_key = module.application_insights.instrumentation_key 120 | workspace_id = module.log_analytics_workspace.id 121 | dapr_components = [{ 122 | name = var.dapr_name 123 | component_type = var.dapr_component_type 124 | version = var.dapr_version 125 | ignore_errors = var.dapr_ignore_errors 126 | init_timeout = var.dapr_init_timeout 127 | secret = [ 128 | { 129 | name = "storageaccountkey" 130 | value = module.storage_account.primary_access_key 131 | } 132 | ] 133 | metadata: [ 134 | { 135 | name = "accountName" 136 | value = module.storage_account.name 137 | }, 138 | { 139 | name = "containerName" 140 | value = var.container_name 141 | }, 142 | { 143 | name = "accountKey" 144 | secret_name = "storageaccountkey" 145 | } 146 | ] 147 | scopes = var.dapr_scopes 148 | }] 149 | container_apps = var.container_apps 150 | } -------------------------------------------------------------------------------- /modules/application_insights/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.43.0" 7 | } 8 | } 9 | } 10 | 11 | locals { 12 | module_tag = { 13 | "module" = basename(abspath(path.module)) 14 | } 15 | tags = merge(var.tags, local.module_tag) 16 | } 17 | 18 | resource "azurerm_application_insights" "resource" { 19 | name = var.name 20 | location = var.location 21 | resource_group_name = var.resource_group_name 22 | tags = local.tags 23 | application_type = "web" 24 | workspace_id = var.workspace_id 25 | 26 | lifecycle { 27 | ignore_changes = [ 28 | tags 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /modules/application_insights/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | value = azurerm_application_insights.resource.name 3 | description = "Specifies the name of the resource." 4 | } 5 | 6 | output "id" { 7 | value = azurerm_application_insights.resource.id 8 | description = "Specifies the resource id of the resource." 9 | } 10 | 11 | output "instrumentation_key" { 12 | value = azurerm_application_insights.resource.instrumentation_key 13 | description = "Specifies the instrumentation key of the Application Insights." 14 | } 15 | 16 | output "app_id" { 17 | value = azurerm_application_insights.resource.app_id 18 | description = "Specifies the resource id of the resource." 19 | } -------------------------------------------------------------------------------- /modules/application_insights/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "name" { 3 | description = "(Required) Specifies the name of the resource. Changing this forces a new resource to be created." 4 | type = string 5 | } 6 | 7 | variable "resource_group_name" { 8 | description = "(Required) The name of the resource group in which to create the resource. Changing this forces a new resource to be created." 9 | type = string 10 | } 11 | 12 | variable "tags" { 13 | description = "(Optional) Specifies the tags of the log analytics workspace" 14 | type = map(any) 15 | default = {} 16 | } 17 | 18 | variable "location" { 19 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 20 | type = string 21 | } 22 | 23 | variable "application_type" { 24 | description = "(Required) Specifies the type of Application Insights to create. Valid values are ios for iOS, java for Java web, MobileCenter for App Center, Node.JS for Node.js, other for General, phone for Windows Phone, store for Windows Store and web for ASP.NET. Please note these values are case sensitive; unmatched values are treated as ASP.NET by Azure. Changing this forces a new resource to be created." 25 | type = string 26 | default = "web" 27 | } 28 | 29 | variable "workspace_id" { 30 | description = "(Optional) Specifies the id of a log analytics workspace resource. Changing this forces a new resource to be created." 31 | type = string 32 | } 33 | -------------------------------------------------------------------------------- /modules/container_apps/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.43.0" 7 | } 8 | azapi = { 9 | source = "azure/azapi" 10 | } 11 | } 12 | } 13 | 14 | resource "azurerm_container_app_environment" "managed_environment" { 15 | name = var.managed_environment_name 16 | location = var.location 17 | resource_group_name = var.resource_group_name 18 | log_analytics_workspace_id = var.workspace_id 19 | infrastructure_subnet_id = var.infrastructure_subnet_id 20 | internal_load_balancer_enabled = var.internal_load_balancer_enabled 21 | tags = var.tags 22 | 23 | lifecycle { 24 | ignore_changes = [ 25 | tags 26 | ] 27 | } 28 | } 29 | 30 | resource "azurerm_container_app_environment_dapr_component" "dapr_component" { 31 | for_each = {for component in var.dapr_components: component.name => component} 32 | 33 | name = each.key 34 | container_app_environment_id = azurerm_container_app_environment.managed_environment.id 35 | component_type = each.value.component_type 36 | version = each.value.version 37 | ignore_errors = each.value.ignore_errors 38 | init_timeout = each.value.init_timeout 39 | scopes = each.value.scopes 40 | 41 | dynamic "metadata" { 42 | for_each = each.value.metadata != null ? each.value.metadata : [] 43 | content { 44 | name = metadata.value.name 45 | secret_name = try(metadata.value.secret_name, null) 46 | value = try(metadata.value.value, null) 47 | } 48 | } 49 | 50 | dynamic "secret" { 51 | for_each = each.value.secret != null ? each.value.secret : [] 52 | content { 53 | name = secret.value.name 54 | value = secret.value.value 55 | } 56 | } 57 | } 58 | 59 | resource "azurerm_container_app" "container_app" { 60 | for_each = {for app in var.container_apps: app.name => app} 61 | 62 | name = each.key 63 | resource_group_name = var.resource_group_name 64 | container_app_environment_id = azurerm_container_app_environment.managed_environment.id 65 | tags = var.tags 66 | revision_mode = each.value.revision_mode 67 | 68 | template { 69 | dynamic "container" { 70 | for_each = coalesce(each.value.template.containers, []) 71 | content { 72 | name = container.value.name 73 | image = container.value.image 74 | args = try(container.value.args, null) 75 | command = try(container.value.command, null) 76 | cpu = container.value.cpu 77 | memory = container.value.memory 78 | 79 | dynamic "env" { 80 | for_each = coalesce(container.value.env, []) 81 | content { 82 | name = env.value.name 83 | secret_name = try(env.value.secret_name, null) 84 | value = try(env.value.value, null) 85 | } 86 | } 87 | } 88 | } 89 | min_replicas = try(each.value.template.min_replicas, null) 90 | max_replicas = try(each.value.template.max_replicas, null) 91 | revision_suffix = try(each.value.template.revision_suffix, null) 92 | 93 | dynamic "volume" { 94 | for_each = each.value.template.volume != null ? [each.value.template.volume] : [] 95 | content { 96 | name = volume.value.name 97 | storage_name = try(volume.value.storage_name, null) 98 | storage_type = try(volume.value.storage_type, null) 99 | } 100 | } 101 | } 102 | 103 | dynamic "ingress" { 104 | for_each = each.value.ingress != null ? [each.value.ingress] : [] 105 | content { 106 | allow_insecure_connections = try(ingress.value.allow_insecure_connections, null) 107 | external_enabled = try(ingress.value.external_enabled, null) 108 | target_port = ingress.value.target_port 109 | transport = ingress.value.transport 110 | 111 | dynamic "traffic_weight" { 112 | for_each = coalesce(ingress.value.traffic_weight, []) 113 | content { 114 | label = traffic_weight.value.label 115 | latest_revision = traffic_weight.value.latest_revision 116 | revision_suffix = traffic_weight.value.revision_suffix 117 | percentage = traffic_weight.value.percentage 118 | } 119 | } 120 | } 121 | } 122 | 123 | dynamic "dapr" { 124 | for_each = each.value.dapr != null ? [each.value.dapr] : [] 125 | content { 126 | app_id = dapr.value.app_id 127 | app_port = dapr.value.app_port 128 | app_protocol = dapr.value.app_protocol 129 | } 130 | } 131 | 132 | dynamic "secret" { 133 | for_each = each.value.secrets != null ? [each.value.secrets] : [] 134 | content { 135 | name = secret.value.name 136 | value = secret.value.value 137 | } 138 | } 139 | 140 | lifecycle { 141 | ignore_changes = [ 142 | tags 143 | ] 144 | } 145 | } 146 | 147 | resource "azapi_update_resource" "containerapp" { 148 | type = "Microsoft.App/containerApps@2022-10-01" 149 | resource_id = azurerm_container_app.container_app["pythonapp"].id 150 | 151 | body = jsonencode({ 152 | properties = { 153 | configuration = { 154 | dapr = { 155 | appPort = null 156 | } 157 | } 158 | } 159 | }) 160 | 161 | depends_on = [ 162 | azurerm_container_app.container_app["pythonapp"], 163 | ] 164 | } 165 | -------------------------------------------------------------------------------- /modules/container_apps/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | value = azurerm_container_app_environment.managed_environment.name 3 | description = "Specifies the name of the managed environment." 4 | } 5 | 6 | output "id" { 7 | value = azurerm_container_app_environment.managed_environment.id 8 | description = "Specifies the resource id of the managed environment." 9 | } -------------------------------------------------------------------------------- /modules/container_apps/variables.tf: -------------------------------------------------------------------------------- 1 | 2 | variable "managed_environment_name" { 3 | description = "(Required) Specifies the name of the managed environment." 4 | type = string 5 | } 6 | 7 | variable "resource_group_name" { 8 | description = "(Required) Specifies the resource group name" 9 | type = string 10 | } 11 | 12 | variable "tags" { 13 | description = "(Optional) Specifies the tags of the log analytics workspace" 14 | type = map(any) 15 | default = {} 16 | } 17 | 18 | variable "location" { 19 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 20 | type = string 21 | } 22 | 23 | variable "infrastructure_subnet_id" { 24 | description = "(Optional) Specifies resource id of the subnet hosting the Azure Container Apps environment." 25 | type = string 26 | } 27 | 28 | variable "internal_load_balancer_enabled" { 29 | description = "(Optional) Should the Container Environment operate in Internal Load Balancing Mode? Defaults to false. Changing this forces a new resource to be created." 30 | type = bool 31 | default = false 32 | } 33 | 34 | variable "instrumentation_key" { 35 | description = "(Optional) Specifies the instrumentation key of the application insights resource." 36 | type = string 37 | } 38 | 39 | variable "workspace_id" { 40 | description = "(Optional) Specifies resource id of the log analytics workspace." 41 | type = string 42 | } 43 | 44 | variable "dapr_components" { 45 | description = "(Optional) Specifies the dapr components." 46 | type = list(object({ 47 | name = string 48 | component_type = string 49 | ignore_errors = optional(bool) 50 | version = optional(string) 51 | init_timeout = optional(string) 52 | scopes = optional(list(string)) 53 | metadata = optional(list(object({ 54 | name = string 55 | secret_name = optional(string) 56 | value = optional(string) 57 | }))) 58 | secret = optional(list(object({ 59 | name = string 60 | value = string 61 | }))) 62 | })) 63 | } 64 | 65 | variable "container_apps" { 66 | description = "Specifies the container apps in the managed environment." 67 | type = list(object({ 68 | name = string 69 | revision_mode = optional(string) 70 | ingress = optional(object({ 71 | allow_insecure_connections = optional(bool) 72 | external_enabled = optional(bool) 73 | target_port = optional(number) 74 | transport = optional(string) 75 | traffic_weight = optional(list(object({ 76 | label = optional(string) 77 | latest_revision = optional(bool) 78 | revision_suffix = optional(string) 79 | percentage = optional(number) 80 | }))) 81 | })) 82 | dapr = optional(object({ 83 | app_id = optional(string) 84 | app_port = optional(number) 85 | app_protocol = optional(string) 86 | })) 87 | secrets = optional(list(object({ 88 | name = string 89 | value = string 90 | }))) 91 | template = object({ 92 | containers = list(object({ 93 | name = string 94 | image = string 95 | args = optional(list(string)) 96 | command = optional(list(string)) 97 | cpu = optional(number) 98 | memory = optional(string) 99 | env = optional(list(object({ 100 | name = string 101 | secret_name = optional(string) 102 | value = optional(string) 103 | }))) 104 | })) 105 | min_replicas = optional(number) 106 | max_replicas = optional(number) 107 | revision_suffix = optional(string) 108 | volume = optional(list(object({ 109 | name = string 110 | storage_name = optional(string) 111 | storage_type = optional(string) 112 | }))) 113 | }) 114 | })) 115 | } -------------------------------------------------------------------------------- /modules/log_analytics/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.43.0" 7 | } 8 | } 9 | } 10 | 11 | locals { 12 | module_tag = { 13 | "module" = basename(abspath(path.module)) 14 | } 15 | tags = merge(var.tags, local.module_tag) 16 | } 17 | 18 | resource "azurerm_log_analytics_workspace" "log_analytics_workspace" { 19 | name = var.name 20 | location = var.location 21 | resource_group_name = var.resource_group_name 22 | sku = var.sku 23 | tags = local.tags 24 | retention_in_days = var.retention_in_days != "" ? var.retention_in_days : null 25 | 26 | lifecycle { 27 | ignore_changes = [ 28 | tags 29 | ] 30 | } 31 | } -------------------------------------------------------------------------------- /modules/log_analytics/output.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | value = azurerm_log_analytics_workspace.log_analytics_workspace.id 3 | description = "Specifies the resource id of the log analytics workspace" 4 | } 5 | 6 | output "location" { 7 | value = azurerm_log_analytics_workspace.log_analytics_workspace.location 8 | description = "Specifies the location of the log analytics workspace" 9 | } 10 | 11 | output "name" { 12 | value = azurerm_log_analytics_workspace.log_analytics_workspace.name 13 | description = "Specifies the name of the log analytics workspace" 14 | } 15 | 16 | output "resource_group_name" { 17 | value = azurerm_log_analytics_workspace.log_analytics_workspace.resource_group_name 18 | description = "Specifies the name of the resource group that contains the log analytics workspace" 19 | } 20 | 21 | output "workspace_id" { 22 | value = azurerm_log_analytics_workspace.log_analytics_workspace.workspace_id 23 | description = "Specifies the workspace id of the log analytics workspace" 24 | } 25 | 26 | output "primary_shared_key" { 27 | value = azurerm_log_analytics_workspace.log_analytics_workspace.primary_shared_key 28 | description = "Specifies the workspace key of the log analytics workspace" 29 | sensitive = true 30 | } 31 | -------------------------------------------------------------------------------- /modules/log_analytics/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "(Required) Specifies the name of the log analytics workspace" 3 | type = string 4 | } 5 | 6 | variable "resource_group_name" { 7 | description = "(Required) Specifies the resource group name" 8 | type = string 9 | } 10 | 11 | variable "location" { 12 | description = "(Required) Specifies the location of the log analytics workspace" 13 | type = string 14 | } 15 | 16 | variable "sku" { 17 | description = "(Optional) Specifies the sku of the log analytics workspace" 18 | type = string 19 | default = "PerGB2018" 20 | 21 | validation { 22 | condition = contains(["Free", "Standalone", "PerNode", "PerGB2018"], var.sku) 23 | error_message = "The log analytics sku is incorrect." 24 | } 25 | } 26 | 27 | variable "tags" { 28 | description = "(Optional) Specifies the tags of the log analytics workspace" 29 | type = map(any) 30 | default = {} 31 | } 32 | 33 | variable "retention_in_days" { 34 | description = " (Optional) Specifies the workspace data retention in days. Possible values are either 7 (Free Tier only) or range between 30 and 730." 35 | type = number 36 | default = 30 37 | } -------------------------------------------------------------------------------- /modules/private_dns_zone/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.43.0" 7 | } 8 | } 9 | } 10 | 11 | resource "azurerm_private_dns_zone" "private_dns_zone" { 12 | name = var.name 13 | resource_group_name = var.resource_group_name 14 | tags = var.tags 15 | 16 | lifecycle { 17 | ignore_changes = [ 18 | tags 19 | ] 20 | } 21 | } 22 | 23 | resource "azurerm_private_dns_zone_virtual_network_link" "link" { 24 | for_each = var.virtual_networks_to_link 25 | 26 | name = "link_to_${lower(basename(each.key))}" 27 | resource_group_name = var.resource_group_name 28 | private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone.name 29 | virtual_network_id = "/subscriptions/${each.value.subscription_id}/resourceGroups/${each.value.resource_group_name}/providers/Microsoft.Network/virtualNetworks/${each.key}" 30 | 31 | lifecycle { 32 | ignore_changes = [ 33 | tags 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /modules/private_dns_zone/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "Specifies the resource id of the private dns zone" 3 | value = azurerm_private_dns_zone.private_dns_zone.id 4 | } -------------------------------------------------------------------------------- /modules/private_dns_zone/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "(Required) Specifies the name of the private dns zone" 3 | type = string 4 | } 5 | 6 | variable "resource_group_name" { 7 | description = "(Required) Specifies the resource group name of the private dns zone" 8 | type = string 9 | } 10 | 11 | variable "tags" { 12 | description = "(Optional) Specifies the tags of the private dns zone" 13 | default = {} 14 | } 15 | 16 | variable "virtual_networks_to_link" { 17 | description = "(Optional) Specifies the subscription id, resource group name, and name of the virtual networks to which create a virtual network link" 18 | type = map(any) 19 | default = {} 20 | } -------------------------------------------------------------------------------- /modules/private_endpoint/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.43.0" 7 | } 8 | } 9 | } 10 | 11 | resource "azurerm_private_endpoint" "private_endpoint" { 12 | name = var.name 13 | location = var.location 14 | resource_group_name = var.resource_group_name 15 | subnet_id = var.subnet_id 16 | tags = var.tags 17 | 18 | private_service_connection { 19 | name = "${var.name}Connection" 20 | private_connection_resource_id = var.private_connection_resource_id 21 | is_manual_connection = var.is_manual_connection 22 | subresource_names = try([var.subresource_name], null) 23 | request_message = try(var.request_message, null) 24 | } 25 | 26 | private_dns_zone_group { 27 | name = var.private_dns_zone_group_name 28 | private_dns_zone_ids = var.private_dns_zone_group_ids 29 | } 30 | 31 | lifecycle { 32 | ignore_changes = [ 33 | tags 34 | ] 35 | } 36 | } -------------------------------------------------------------------------------- /modules/private_endpoint/outputs.tf: -------------------------------------------------------------------------------- 1 | output "id" { 2 | description = "Specifies the resource id of the private endpoint." 3 | value = azurerm_private_endpoint.private_endpoint.id 4 | } 5 | 6 | output "private_dns_zone_group" { 7 | description = "Specifies the private dns zone group of the private endpoint." 8 | value = azurerm_private_endpoint.private_endpoint.private_dns_zone_group 9 | } 10 | 11 | output "private_dns_zone_configs" { 12 | description = "Specifies the private dns zone(s) configuration" 13 | value = azurerm_private_endpoint.private_endpoint.private_dns_zone_configs 14 | } -------------------------------------------------------------------------------- /modules/private_endpoint/variables.tf: -------------------------------------------------------------------------------- 1 | variable "name" { 2 | description = "(Required) Specifies the name of the private endpoint. Changing this forces a new resource to be created." 3 | type = string 4 | } 5 | 6 | variable "resource_group_name" { 7 | description = "(Required) The name of the resource group. Changing this forces a new resource to be created." 8 | type = string 9 | } 10 | 11 | variable "private_connection_resource_id" { 12 | description = "(Required) Specifies the resource id of the private link service" 13 | type = string 14 | } 15 | 16 | variable "location" { 17 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 18 | type = string 19 | } 20 | 21 | variable "subnet_id" { 22 | description = "(Required) Specifies the resource id of the subnet" 23 | type = string 24 | } 25 | 26 | variable "is_manual_connection" { 27 | description = "(Optional) Specifies whether the private endpoint connection requires manual approval from the remote resource owner." 28 | type = string 29 | default = false 30 | } 31 | 32 | variable "subresource_name" { 33 | description = "(Optional) Specifies a subresource name which the Private Endpoint is able to connect to." 34 | type = string 35 | default = null 36 | } 37 | 38 | variable "request_message" { 39 | description = "(Optional) Specifies a message passed to the owner of the remote resource when the private endpoint attempts to establish the connection to the remote resource." 40 | type = string 41 | default = null 42 | } 43 | 44 | variable "private_dns_zone_group_name" { 45 | description = "(Required) Specifies the Name of the Private DNS Zone Group. Changing this forces a new private_dns_zone_group resource to be created." 46 | type = string 47 | } 48 | 49 | variable "private_dns_zone_group_ids" { 50 | description = "(Required) Specifies the list of Private DNS Zones to include within the private_dns_zone_group." 51 | type = list(string) 52 | } 53 | 54 | variable "tags" { 55 | description = "(Optional) Specifies the tags of the network security group" 56 | default = {} 57 | } 58 | 59 | variable "private_dns" { 60 | default = {} 61 | } -------------------------------------------------------------------------------- /modules/storage_account/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.43.0" 7 | } 8 | } 9 | } 10 | 11 | resource "azurerm_storage_account" "storage_account" { 12 | name = var.name 13 | resource_group_name = var.resource_group_name 14 | 15 | location = var.location 16 | account_kind = var.account_kind 17 | account_tier = var.account_tier 18 | account_replication_type = var.replication_type 19 | is_hns_enabled = var.is_hns_enabled 20 | tags = var.tags 21 | 22 | network_rules { 23 | default_action = (length(var.ip_rules) + length(var.virtual_network_subnet_ids)) > 0 ? "Deny" : var.default_action 24 | ip_rules = var.ip_rules 25 | virtual_network_subnet_ids = var.virtual_network_subnet_ids 26 | } 27 | 28 | identity { 29 | type = "SystemAssigned" 30 | } 31 | 32 | lifecycle { 33 | ignore_changes = [ 34 | tags 35 | ] 36 | } 37 | } -------------------------------------------------------------------------------- /modules/storage_account/outputs.tf: -------------------------------------------------------------------------------- 1 | output "name" { 2 | description = "Specifies the name of the storage account." 3 | value = azurerm_storage_account.storage_account.name 4 | } 5 | 6 | output "id" { 7 | description = "Specifies the resource id of the storage account." 8 | value = azurerm_storage_account.storage_account.id 9 | } 10 | 11 | output "primary_access_key" { 12 | description = "Specifies the primary access key of the storage account." 13 | value = azurerm_storage_account.storage_account.primary_access_key 14 | } 15 | 16 | output "principal_id" { 17 | description = "Specifies the principal id of the system assigned managed identity of the storage account." 18 | value = azurerm_storage_account.storage_account.identity[0].principal_id 19 | } 20 | 21 | output "primary_blob_endpoint" { 22 | description = "Specifies the primary blob endpoint of the storage account." 23 | value = azurerm_storage_account.storage_account.primary_blob_endpoint 24 | } -------------------------------------------------------------------------------- /modules/storage_account/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | description = "(Required) Specifies the resource group name of the storage account" 3 | type = string 4 | } 5 | 6 | variable "name" { 7 | description = "(Required) Specifies the name of the storage account" 8 | type = string 9 | } 10 | 11 | variable "location" { 12 | description = "(Required) Specifies the location of the storage account" 13 | type = string 14 | } 15 | 16 | variable "account_kind" { 17 | description = "(Optional) Specifies the account kind of the storage account" 18 | default = "StorageV2" 19 | type = string 20 | 21 | validation { 22 | condition = contains(["Storage", "StorageV2"], var.account_kind) 23 | error_message = "The account kind of the storage account is invalid." 24 | } 25 | } 26 | 27 | variable "account_tier" { 28 | description = "(Optional) Specifies the account tier of the storage account" 29 | default = "Standard" 30 | type = string 31 | 32 | validation { 33 | condition = contains(["Standard", "Premium"], var.account_tier) 34 | error_message = "The account tier of the storage account is invalid." 35 | } 36 | } 37 | 38 | variable "replication_type" { 39 | description = "(Optional) Specifies the replication type of the storage account" 40 | default = "LRS" 41 | type = string 42 | 43 | validation { 44 | condition = contains(["LRS", "ZRS", "GRS", "GZRS", "RA-GRS", "RA-GZRS"], var.replication_type) 45 | error_message = "The replication type of the storage account is invalid." 46 | } 47 | } 48 | 49 | variable "is_hns_enabled" { 50 | description = "(Optional) Specifies the replication type of the storage account" 51 | default = false 52 | type = bool 53 | } 54 | 55 | variable "default_action" { 56 | description = "Allow or disallow public access to all blobs or containers in the storage accounts. The default interpretation is true for this property." 57 | default = "Allow" 58 | type = string 59 | } 60 | 61 | variable "ip_rules" { 62 | description = "Specifies IP rules for the storage account" 63 | default = [] 64 | type = list(string) 65 | } 66 | 67 | variable "virtual_network_subnet_ids" { 68 | description = "Specifies a list of resource ids for subnets" 69 | default = [] 70 | type = list(string) 71 | } 72 | 73 | variable "kind" { 74 | description = "(Optional) Specifies the kind of the storage account" 75 | default = "" 76 | } 77 | 78 | variable "tags" { 79 | description = "(Optional) Specifies the tags of the storage account" 80 | default = {} 81 | } -------------------------------------------------------------------------------- /modules/virtual_network/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.3" 3 | required_providers { 4 | azurerm = { 5 | source = "hashicorp/azurerm" 6 | version = "~> 3.43.0" 7 | } 8 | } 9 | } 10 | 11 | resource "azurerm_virtual_network" "vnet" { 12 | name = var.vnet_name 13 | address_space = var.address_space 14 | location = var.location 15 | resource_group_name = var.resource_group_name 16 | tags = var.tags 17 | 18 | lifecycle { 19 | ignore_changes = [ 20 | tags 21 | ] 22 | } 23 | } 24 | 25 | resource "azurerm_subnet" "subnet" { 26 | for_each = { for subnet in var.subnets : subnet.name => subnet } 27 | 28 | name = each.key 29 | resource_group_name = var.resource_group_name 30 | virtual_network_name = azurerm_virtual_network.vnet.name 31 | address_prefixes = each.value.address_prefixes 32 | private_endpoint_network_policies_enabled = each.value.private_endpoint_network_policies_enabled 33 | private_link_service_network_policies_enabled = each.value.private_link_service_network_policies_enabled 34 | } 35 | 36 | resource "azurerm_monitor_diagnostic_setting" "settings" { 37 | name = "DiagnosticsSettings" 38 | target_resource_id = azurerm_virtual_network.vnet.id 39 | log_analytics_workspace_id = var.log_analytics_workspace_id 40 | 41 | enabled_log { 42 | category = "VMProtectionAlerts" 43 | 44 | retention_policy { 45 | enabled = true 46 | days = var.log_analytics_retention_days 47 | } 48 | } 49 | 50 | metric { 51 | category = "AllMetrics" 52 | 53 | retention_policy { 54 | enabled = true 55 | days = var.log_analytics_retention_days 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /modules/virtual_network/outputs.tf: -------------------------------------------------------------------------------- 1 | output name { 2 | description = "Specifies the name of the virtual network" 3 | value = azurerm_virtual_network.vnet.name 4 | } 5 | 6 | output vnet_id { 7 | description = "Specifies the resource id of the virtual network" 8 | value = azurerm_virtual_network.vnet.id 9 | } 10 | 11 | output subnet_ids { 12 | description = "Contains a list of the the resource id of the subnets" 13 | value = { for subnet in azurerm_subnet.subnet : subnet.name => subnet.id } 14 | } -------------------------------------------------------------------------------- /modules/virtual_network/variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_group_name" { 2 | description = "Resource Group name" 3 | type = string 4 | } 5 | 6 | variable "location" { 7 | description = "Location in which to deploy the network" 8 | type = string 9 | } 10 | 11 | variable "vnet_name" { 12 | description = "VNET name" 13 | type = string 14 | } 15 | 16 | variable "address_space" { 17 | description = "VNET address space" 18 | type = list(string) 19 | } 20 | 21 | variable "subnets" { 22 | description = "Subnets configuration" 23 | type = list(object({ 24 | name = string 25 | address_prefixes = list(string) 26 | private_endpoint_network_policies_enabled = bool 27 | private_link_service_network_policies_enabled = bool 28 | })) 29 | } 30 | 31 | variable "tags" { 32 | description = "(Optional) Specifies the tags of the storage account" 33 | default = {} 34 | } 35 | 36 | variable "log_analytics_workspace_id" { 37 | description = "Specifies the log analytics workspace id" 38 | type = string 39 | } 40 | 41 | variable "log_analytics_retention_days" { 42 | description = "Specifies the number of days of the retention policy" 43 | type = number 44 | default = 7 45 | } -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "log_analytics_name" { 2 | value = module.log_analytics_workspace.name 3 | } 4 | 5 | output "log_analytics_workspace_id" { 6 | value = module.log_analytics_workspace.workspace_id 7 | } 8 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "resource_prefix" { 2 | description = "Specifies a prefix for all the resource names." 3 | type = string 4 | } 5 | 6 | variable "location" { 7 | description = "(Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created." 8 | type = string 9 | default = "WestEurope" 10 | } 11 | 12 | variable "resource_group_name" { 13 | description = "Name of the resource group in which the resources will be created" 14 | default = "RG" 15 | } 16 | 17 | variable "tags" { 18 | description = "(Optional) Specifies tags for all the resources" 19 | default = { 20 | createdWith = "Terraform" 21 | } 22 | } 23 | 24 | variable "log_analytics_workspace_name" { 25 | description = "Specifies the name of the log analytics workspace" 26 | default = "Workspace" 27 | type = string 28 | } 29 | 30 | variable "log_analytics_retention_days" { 31 | description = "Specifies the number of days of the retention policy for the log analytics workspace." 32 | type = number 33 | default = 30 34 | } 35 | 36 | variable "application_insights_name" { 37 | description = "Specifies the name of the application insights resource." 38 | default = "ApplicationInsights" 39 | type = string 40 | } 41 | 42 | variable "application_insights_application_type" { 43 | description = "(Required) Specifies the type of Application Insights to create. Valid values are ios for iOS, java for Java web, MobileCenter for App Center, Node.JS for Node.js, other for General, phone for Windows Phone, store for Windows Store and web for ASP.NET. Please note these values are case sensitive; unmatched values are treated as ASP.NET by Azure. Changing this forces a new resource to be created." 44 | type = string 45 | default = "web" 46 | } 47 | 48 | variable "vnet_name" { 49 | description = "Specifies the name of the virtual network" 50 | default = "VNet" 51 | type = string 52 | } 53 | 54 | variable "vnet_address_space" { 55 | description = "Specifies the address prefix of the virtual network" 56 | default = ["10.0.0.0/16"] 57 | type = list(string) 58 | } 59 | 60 | variable "aca_subnet_name" { 61 | description = "Specifies the name of the subnet" 62 | default = "ContainerApps" 63 | type = string 64 | } 65 | 66 | variable "aca_subnet_address_prefix" { 67 | description = "Specifies the address prefix of the Azure Container Apps environment subnet" 68 | default = ["10.0.0.0/20"] 69 | type = list(string) 70 | } 71 | 72 | variable "private_endpoint_subnet_name" { 73 | description = "Specifies the name of the subnet" 74 | default = "PrivateEndpoints" 75 | type = string 76 | } 77 | 78 | variable "private_endpoint_subnet_address_prefix" { 79 | description = "Specifies the address prefix of the private endpoints subnet" 80 | default = ["10.0.16.0/24"] 81 | type = list(string) 82 | } 83 | 84 | variable "storage_account_name" { 85 | description = "(Optional) Specifies the name of the storage account" 86 | default = "account" 87 | type = string 88 | } 89 | 90 | variable "storage_account_replication_type" { 91 | description = "(Optional) Specifies the replication type of the storage account" 92 | default = "LRS" 93 | type = string 94 | 95 | validation { 96 | condition = contains(["LRS", "ZRS", "GRS", "GZRS", "RA-GRS", "RA-GZRS"], var.storage_account_replication_type) 97 | error_message = "The replication type of the storage account is invalid." 98 | } 99 | } 100 | 101 | variable "storage_account_kind" { 102 | description = "(Optional) Specifies the account kind of the storage account" 103 | default = "StorageV2" 104 | type = string 105 | 106 | validation { 107 | condition = contains(["Storage", "StorageV2"], var.storage_account_kind) 108 | error_message = "The account kind of the storage account is invalid." 109 | } 110 | } 111 | 112 | variable "storage_account_tier" { 113 | description = "(Optional) Specifies the account tier of the storage account" 114 | default = "Standard" 115 | type = string 116 | 117 | validation { 118 | condition = contains(["Standard", "Premium"], var.storage_account_tier) 119 | error_message = "The account tier of the storage account is invalid." 120 | } 121 | } 122 | 123 | variable "managed_environment_name" { 124 | description = "(Required) Specifies the name of the managed environment." 125 | type = string 126 | default = "ManagedEnvironment" 127 | } 128 | 129 | variable "internal_load_balancer_enabled" { 130 | description = "(Optional) Should the Container Environment operate in Internal Load Balancing Mode? Defaults to false. Changing this forces a new resource to be created." 131 | type = bool 132 | default = false 133 | } 134 | 135 | variable "dapr_name" { 136 | description = "(Required) Specifies the name of the dapr component." 137 | type = string 138 | default = "statestore" 139 | } 140 | 141 | variable "dapr_component_type" { 142 | description = "(Required) Specifies the type of the dapr component." 143 | type = string 144 | default = "state.azure.blobstorage" 145 | } 146 | 147 | variable "dapr_ignore_errors" { 148 | description = "(Required) Specifies if the component errors are ignored." 149 | type = bool 150 | default = false 151 | } 152 | 153 | variable "dapr_version" { 154 | description = "(Required) Specifies the version of the dapr component." 155 | type = string 156 | default = "v1" 157 | } 158 | 159 | variable "dapr_init_timeout" { 160 | description = "(Required) Specifies the init timeout of the dapr component." 161 | type = string 162 | default = "5s" 163 | } 164 | 165 | variable "dapr_scopes" { 166 | description = "(Required) Specifies the init timeout of the dapr component." 167 | type = list 168 | default = ["nodeapp"] 169 | } 170 | 171 | variable "container_name" { 172 | description = "Specifies the name of the container in the storage account." 173 | type = string 174 | default = "state" 175 | } 176 | 177 | variable "container_access_type" { 178 | description = "Specifies the access type of the container in the storage account." 179 | type = string 180 | default = "private" 181 | } 182 | 183 | variable "container_apps" { 184 | description = "Specifies the container apps in the managed environment." 185 | type = list(object({ 186 | name = string 187 | revision_mode = optional(string) 188 | ingress = optional(object({ 189 | allow_insecure_connections = optional(bool) 190 | external_enabled = optional(bool) 191 | target_port = optional(number) 192 | transport = optional(string) 193 | traffic_weight = optional(list(object({ 194 | label = optional(string) 195 | latest_revision = optional(bool) 196 | revision_suffix = optional(string) 197 | percentage = optional(number) 198 | }))) 199 | })) 200 | dapr = optional(object({ 201 | app_id = optional(string) 202 | app_port = optional(number) 203 | app_protocol = optional(string) 204 | })) 205 | secrets = optional(list(object({ 206 | name = string 207 | value = string 208 | }))) 209 | template = object({ 210 | containers = list(object({ 211 | name = string 212 | image = string 213 | args = optional(list(string)) 214 | command = optional(list(string)) 215 | cpu = optional(number) 216 | memory = optional(string) 217 | env = optional(list(object({ 218 | name = string 219 | secret_name = optional(string) 220 | value = optional(string) 221 | }))) 222 | })) 223 | min_replicas = optional(number) 224 | max_replicas = optional(number) 225 | revision_suffix = optional(string) 226 | volume = optional(list(object({ 227 | name = string 228 | storage_name = optional(string) 229 | storage_type = optional(string) 230 | }))) 231 | }) 232 | })) 233 | default = [{ 234 | name = "nodeapp" 235 | revision_mode = "Single" 236 | ingress = { 237 | external_enabled = false 238 | target_port = 3000 239 | transport = "http" 240 | traffic_weight = [{ 241 | label = "blue" 242 | latest_revision = true 243 | revision_suffix = "blue" 244 | percentage = 100 245 | }] 246 | } 247 | dapr = { 248 | app_id = "nodeapp" 249 | app_port = 3000 250 | app_protocol = "http" 251 | } 252 | template = { 253 | containers = [{ 254 | name = "hello-k8s-node" 255 | image = "dapriosamples/hello-k8s-node:latest" 256 | cpu = 0.5 257 | memory = "1Gi" 258 | env = [{ 259 | name = "APP_PORT" 260 | value = 3000 261 | }] 262 | }] 263 | min_replicas = 1 264 | max_replicas = 1 265 | } 266 | }, 267 | { 268 | name = "pythonapp" 269 | revision_mode = "Single" 270 | dapr = { 271 | app_id = "pythonapp" 272 | app_port = 80 273 | } 274 | template = { 275 | containers = [{ 276 | name = "hello-k8s-python" 277 | image = "dapriosamples/hello-k8s-python:latest" 278 | cpu = 0.5 279 | memory = "1Gi" 280 | }] 281 | min_replicas = 1 282 | max_replicas = 1 283 | } 284 | }] 285 | } --------------------------------------------------------------------------------