variable "env" {
description = "Environment"
default = "dev"
}
variable "app_name" {
description = "Application name"
}
variable "subdomain" {
description = "Subdomain for the DNS record"
}
variable "dns_zone" {
description = "DNS Zone name"
}
variable "dns_resource_group" {
description = "DNS resource group"
}
variable "location" {
description = "Location"
default = "Brazil South"
}
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>3.80.0"
}
azapi = {
source = "azure/azapi"
}
}
backend "remote" {}
}
provider "azurerm" {
features {}
}
provider "azapi" {}
resource "azurerm_resource_group" "rg" {
name = "${upper(var.env)}-${upper(var.app_name)}-RG"
location = var.location
}
resource "azurerm_log_analytics_workspace" "logs" {
name = "${upper(var.env)}-${upper(var.app_name)}-logs"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
}
resource "azurerm_container_app_environment" "app_env" {
name = "${upper(var.env)}-${upper(var.app_name)}-CAE"
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
log_analytics_workspace_id = azurerm_log_analytics_workspace.logs.id
}
resource "azurerm_container_app" "app" {
name = "${lower(var.env)}-${lower(var.app_name)}-ca"
container_app_environment_id = azurerm_container_app_environment.app_env.id
resource_group_name = azurerm_resource_group.rg.name
revision_mode = "Single"
template {
container {
name = "${var.env}-${var.app_name}-container"
image = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"
cpu = 0.25
memory = "0.5Gi"
}
}
ingress {
transport = "http"
target_port = 80
external_enabled = true
allow_insecure_connections = false
traffic_weight {
percentage = 100
latest_revision = true
}
}
lifecycle {
ignore_changes = [ingress.0.custom_domain] // Required to not delete the custom domain created in dns.tf
}
}
output "default_app_url" {
value = azurerm_container_app.app.ingress[0].fqdn
}
output "app_url" {
value = time_sleep.dns_propagation.triggers["url"]
}
data "azurerm_dns_zone" "dns" {
name = var.dns_zone
resource_group_name = var.dns_resource_group
}
resource "azurerm_dns_cname_record" "cname_record" {
name = var.subdomain
zone_name = data.azurerm_dns_zone.dns.name
resource_group_name = data.azurerm_dns_zone.dns.resource_group_name
ttl = 3600
record = azurerm_container_app.app.ingress[0].fqdn
}
data "azapi_resource" "app_verification_id" {
resource_id = azurerm_container_app_environment.app_env.id
type = "Microsoft.App/managedEnvironments@2023-05-01"
response_export_values = ["properties.customDomainConfiguration.customDomainVerificationId"]
}
locals {
verificationId = jsondecode(data.azapi_resource.app_verification_id.output).properties.customDomainConfiguration.customDomainVerificationId
}
resource "azurerm_dns_txt_record" "txt_record" {
name = "asuid.${var.subdomain}"
zone_name = data.azurerm_dns_zone.dns.name
resource_group_name = data.azurerm_dns_zone.dns.resource_group_name
ttl = 3600
record {
value = local.verificationId
}
}
resource "time_sleep" "dns_propagation" {
create_duration = "60s"
depends_on = [azurerm_dns_txt_record.txt_record, azurerm_dns_cname_record.cname_record]
triggers = {
url = "${azurerm_dns_cname_record.cname_record.name}.${data.azurerm_dns_zone.dns.name}",
verificationId = local.verificationId,
record = azurerm_dns_cname_record.cname_record.record,
}
}
// azurerm can't create a managed TLS certificate - see https://github.com/hashicorp/terraform-provider-azurerm/issues/21866
// The following resources are the workaround
resource "azapi_update_resource" "custom_domain" {
type = "Microsoft.App/containerApps@2023-05-01"
resource_id = azurerm_container_app.app.id
body = jsonencode({
properties = {
ingress = {
customDomains = [
{
bindingType = "Disabled",
name = time_sleep.dns_propagation.triggers["url"],
}
]
}
}
})
}
resource "azapi_resource" "managed_certificate" {
depends_on = [time_sleep.dns_propagation, azapi_update_resource.custom_domain]
type = "Microsoft.App/ManagedEnvironments/managedCertificates@2023-05-01"
name = "${lower(var.env)}-${lower(var.app_name)}-cert"
parent_id = azurerm_container_app_environment.app_env.id
location = azurerm_resource_group.rg.location
body = jsonencode({
properties = {
subjectName = time_sleep.dns_propagation.triggers["url"]
domainControlValidation = "CNAME"
}
})
response_export_values = ["*"]
}
resource "azapi_update_resource" "custom_domain_binding" {
type = "Microsoft.App/containerApps@2023-05-01"
resource_id = azurerm_container_app.app.id
body = jsonencode({
properties = {
ingress = {
customDomains = [
{
bindingType = "SniEnabled",
name = time_sleep.dns_propagation.triggers["url"],
certificateId = jsondecode(azapi_resource.managed_certificate.output).id
}
]
}
}
})
}
This is a superb Gist. Thank you so much for sharing it!
@kneumann-BB you are right, that
configurationblock to surroundingressis needed. That is what made my configuration work. Thank you.I had to make a few changes to get past issues during apply, updates and destroy:
jsonencodeandjsonencodeare no longer necessary (don't work in fact) withbodyargument in the latest version of the azapi provider at the date of this posting.Add this resource to
dns.tf:Replace:
with
Remove the whole
lifecycleblock fromresource "azurerm_container_app" "app"EXPLANATION OF CHANGES:
I think this must be due to an update to the azapi provider
As it is,
terraform destroyfails because managed certificate cannot be destroyed as thecustom_domaininingressis still holding a reference to it. To get around it, I added aazapi_resource_actionresource from AzAPI provider.Like
azapi_update_resource, this is a one time API call but unlikeazapi_update_resource, we can choose whether the action would run on create or destroy. I added an instance that runs ondestroy, but is dependent onazapi_update_resourceso that it definitely runs - and removes thecustom_domainbinding - before destroy of the managed certificate onterraform destroy.This is the bit to add:
There is a problem with updates computed during the plan stage if you run
terraform applyagain (e.g. because there an update to some other resource in the Terraform configuration needs to be made). Terraform computes drift from the first of the twoazapi_update_resourceresources, the one that creates a custom domain binding with abindingTypeofDisabled. Terraform want to set it back toDisabledeven though it was set by the secondazapi_update_resourcetoSniEnabledwith an actualcertificaateIdvalue from the managed cert that had been created.The plan looks like this:
To get around this issue, and allow subsequent updates to the configuration via
terraform applyto happen correctly, replace:with
This warning is shown on
terraform apply:I think this is because Terraform can't control whether or not to include
ingress[0].custom_domainand the provider decides this. The provider I think also does not include it in its drift calculation. If I removelifecycle.ignore_changesfromresource "azurerm_container_app" "app", subsequenttarraform applys, where I have changed other thing in the config, do not include any changes to this resource in the plan, even though there has definitely been drift oningress[0].custom_domainbecause subsequent azapi resources add custom domain binding to the app then modify that binding.Fix: remove the whole
lifecycleblock fromresource "azurerm_container_app" "app"