├── .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 | 
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 | 
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 | }
--------------------------------------------------------------------------------