Last active
July 4, 2025 16:53
-
-
Save naveedausaf/2ac60d1824590d28f625e18ad3a35abc to your computer and use it in GitHub Desktop.
Terraform to create an Azure Container App protected by Cloudflare Free, that uses a user-assigned managed identity to pull image from Azure Container Registry and a secret from Azure Key Vault
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| data "azurerm_resource_group" "core" { | |
| name = var.core_resource_group_name | |
| } | |
| data "azurerm_user_assigned_identity" "app" { | |
| name = var.flowmazon_api_managed_identity | |
| resource_group_name = data.azurerm_resource_group.core.name | |
| } | |
| data "azurerm_key_vault" "app" { | |
| name = var.app_key_vault_name | |
| resource_group_name = data.azurerm_resource_group.core.name | |
| } | |
| data "azurerm_key_vault_secret" "connstr_for_app" { | |
| name = var.key_vault_secretname_connectionstring_for_api | |
| key_vault_id = data.azurerm_key_vault.app.id | |
| } | |
| data "azurerm_container_registry" "app" { | |
| name = var.acr_name | |
| resource_group_name = data.azurerm_resource_group.core.name | |
| } | |
| locals { | |
| full_image_name = "${data.azurerm_container_registry.app.login_server}/${var.image_repository_name}:${var.version_to_deploy}" | |
| } | |
| resource "azurerm_resource_group" "app" { | |
| name = var.app_resource_group_name | |
| location = var.app_resource_group_location | |
| } | |
| resource "azurerm_container_app_environment" "app" { | |
| name = var.app_environment_name | |
| location = azurerm_resource_group.app.location | |
| resource_group_name = azurerm_resource_group.app.name | |
| } | |
| resource "azurerm_container_app" "app" { | |
| name = var.app_name | |
| container_app_environment_id = azurerm_container_app_environment.app.id | |
| resource_group_name = azurerm_resource_group.app.name | |
| revision_mode = var.app_revision_mode | |
| ingress { | |
| external_enabled = true | |
| allow_insecure_connections = false | |
| target_port = var.app_container_port | |
| client_certificate_mode = "require" | |
| dynamic "ip_security_restriction" { | |
| for_each = local.cloudflare_ips | |
| content { | |
| # want to generate a unique name for the | |
| # ip range. Better practice with dynamically | |
| # generated blocks to do this than to | |
| # assign index 1...n that we could have | |
| # done with count instead of for-each | |
| # as we have done in this dynamic block | |
| name = "Allow-Cloudflare-${replace(replace(replace(ip_security_restriction.value, ".", "-"), ":", "--"), "/", "_")}" | |
| description = "One of the CloudFlare's outbound IP ranges. Part of a fairly stable set." | |
| ip_address_range = ip_security_restriction.value | |
| action = "Allow" | |
| } | |
| } | |
| traffic_weight { | |
| latest_revision = true | |
| percentage = 100 | |
| } | |
| # Note this ingress would need to have a custom domain binding | |
| # but we only add it (see use of azapi resources below to do | |
| # that) once DNS records (CNAME and TXT) have been created | |
| # at CloudFlare and have propagated. | |
| # If we do it now we would get an error. | |
| # | |
| # I have verified that once the custom domain has been created, | |
| # subsequent `terraform apply` operations DO NOT detect | |
| # the addition of custom domain to this ingress as drift and | |
| # try to deleted it. So we're ok. | |
| } | |
| registry { | |
| server = data.azurerm_container_registry.app.login_server | |
| identity = data.azurerm_user_assigned_identity.app.id | |
| } | |
| secret { | |
| name = var.key_vault_secretname_connectionstring_for_api | |
| identity = data.azurerm_user_assigned_identity.app.id | |
| key_vault_secret_id = data.azurerm_key_vault_secret.connstr_for_app.id | |
| } | |
| identity { | |
| # it is possible for a service to have both. I am giving it | |
| # both. | |
| # This is because while we need the UserAssigned identity to | |
| # let the app access the key vault and ACR etc, the | |
| # automatically created and assigned system-assigned identity | |
| # of the app may be used in authN/authZ with ACA app environment | |
| # which we have created in a way that its settings would be | |
| # autogenerated and would not be changeable after creation. | |
| type = "SystemAssigned, UserAssigned" | |
| identity_ids = [data.azurerm_user_assigned_identity.app.id] | |
| } | |
| template { | |
| container { | |
| name = var.app_container_name | |
| image = local.full_image_name | |
| cpu = 0.5 | |
| memory = "1Gi" | |
| env { | |
| name = local.allowed_cors_origins_env_var_name | |
| value = var.allowed_cors_origins_for_api | |
| } | |
| env { | |
| name = local.connection_string_env_var_name | |
| # We don't want to set it from the value of the secret | |
| # read from key vault (the account executing this terraform | |
| # config may not even have the permission to do that).value | |
| # | |
| # If we did that, the value would be visible in | |
| # Environment Variables section of the app in plain text | |
| # | |
| # Therefore we simply provide the name of the secret | |
| # we want the value to be retrieved from. | |
| # | |
| # For this to work, we need to reference the secret | |
| # in a secret object in the app object (done above), | |
| # which in turn requries a reference to a | |
| # azurerm_key_vault_secret data source and for that a | |
| # azurerm_key_vault data source. | |
| secret_name = var.key_vault_secretname_connectionstring_for_api | |
| } | |
| liveness_probe { | |
| transport = "HTTP" | |
| port = var.app_container_port | |
| path = "/health/live" | |
| } | |
| readiness_probe { | |
| transport = "HTTP" | |
| port = var.app_container_port | |
| path = "/health/ready" | |
| } | |
| # because of the way liveness probe is implemented in | |
| # the ASP.NET Core API, the liveness probe only | |
| # responds with Healthy and 2xx if | |
| # startup has completed. So we are using it as | |
| # startup_probe also | |
| startup_probe { | |
| transport = "HTTP" | |
| port = var.app_container_port | |
| path = "/health/live" | |
| } | |
| } | |
| } | |
| } | |
| # Create DNS settings in CloudFlare | |
| # PREREQUISITE: CloudFlare's namesrevers should already been rwgistered | |
| # with the Domain Name Registrar you bought the domain name from. | |
| # This would have created a Zone in CLoudFlare for your apex domain | |
| # e.g. `efast.uk` | |
| # returns cloudflare's ip ranges so we can add them to ingress allow rules | |
| # in ACA app | |
| data "cloudflare_ip_ranges" "cloudflare" {} | |
| locals { | |
| # Create a set of Cloudflare IPv4 IPs which we | |
| # will put in the allow list of the container app's ingress | |
| cloudflare_ips = toset(data.cloudflare_ip_ranges.cloudflare.ipv4_cidrs) | |
| subdomain = split(".", var.app_domain_name)[0] # e.g., 'api' for 'api.efast.uk' | |
| apexdomain = trimprefix(var.app_domain_name, format("%s.", local.subdomain)) # e.g., 'efast.uk' | |
| # this is what we would create a TXT record with for the app's domain name | |
| custom_domain_verification_id = azurerm_container_app.app.custom_domain_verification_id | |
| } | |
| # To map the API's domain name to your ACA app, we need to create | |
| # a CNAME record and a TXT record. This is normally done for the | |
| # subdomain part of the app's domain name (for example create a | |
| # CNAME record for `api` and TXT record for `asuid.api` where the | |
| # domain name of the app is api.efast.uk). | |
| # | |
| # HOWEVER, with CLoudFlare's Terraform resources, you have to | |
| # provide the full domain name (FQDN, e.g. api.efast.uk). If | |
| # you only provider the prefix then it works on first apply but | |
| # on every subsequent apply, perhaps because you made a change | |
| # to some other resource, the cloudflare_dns_record resource | |
| # would say there is a change and it has to change | |
| # the current name in the actual CloudFlare DNS record, which | |
| # would be `api.efast.uk` for CNAME record and `asuid.api.efast.uk | |
| # for TXT record, back to `api` and `asuid.api` respectively. | |
| resource "cloudflare_dns_record" "app_cname" { | |
| zone_id = var.cloudflare_zone_id | |
| name = var.app_domain_name | |
| # this should be the target FQDN without the `https://` prefix, | |
| # which is how ingress's fqdn property returns it | |
| content = azurerm_container_app.app.ingress[0].fqdn | |
| type = "CNAME" | |
| ttl = 1 # 1 means TTL is automatically set by CloudFlare | |
| # Setting proxied = true is what allows us to get CloudFlare | |
| # protections such as DDoS protection, WAF and API Shield features. | |
| # | |
| # However we need it to be false initially to allow domain | |
| # name verification to be successful for creating custom | |
| # domain binding on the app's ingress and for managed | |
| # certificate generation. | |
| # Even so, I have a restful_operation resource that, in the | |
| # the multi-step Cloudflare setup process (signposted below) | |
| # turn it off before creating custom domain binding, generating | |
| # managed cert and updating the custom domain binding with it. | |
| # Once that is done, another restful_operation would turn | |
| # it back on. | |
| # SO I could have left proxied to true here. | |
| # | |
| # YET, the reason for leaving it to false at time of CNAME | |
| # record creation is that if I leave it on, then even though | |
| # it is later turned off during the process and turned back | |
| # on again, and during this "blip", DNS record verififcation | |
| # happens successfully, once I turn it bacl on, it took | |
| # a long time, sometimes even 30 minutes, for the DNS cache | |
| # in Cloudflare to be flushed and the domain name to be reachable. | |
| # In the meantime, I would keep getting DNS_PROBE_FINISHED_NXDOMAIN | |
| # error in the browser. | |
| # | |
| # Now this does not happen and as soon as terraform apply has | |
| # finidhed, the domain is instantly reachable (although if | |
| # container count is zero, it can take around 20 seconds for | |
| # a new container to spinup so that request take a while | |
| # to complete). I have verified this by destroying and applying | |
| # again and again many times. After every apply, I was able | |
| # to reach the domain name of the app immediately. | |
| proxied = false | |
| lifecycle { | |
| ignore_changes = [proxied] | |
| } | |
| } | |
| resource "cloudflare_dns_record" "app_txt" { | |
| zone_id = var.cloudflare_zone_id | |
| name = format("%s.%s", "asuid", var.app_domain_name) | |
| # I think, but am not sure, that the content of TXT record | |
| # must be contained within quotes. It works anyway. | |
| content = format("\"%s\"", local.custom_domain_verification_id) | |
| type = "TXT" | |
| ttl = 1 # 1 means TTL is automatically set by CloudFlare | |
| } | |
| # ENABLE mTLS (mutual TLS) | |
| ####################################################################### | |
| # tls_client_auth setting on the zone (beware it applies to whole zone) | |
| # means CloudFlare would present the public key of its own certificate | |
| # to the ACA app when relaying proxied traffic to it. | |
| # The ACA app is set up to require a certificate from the caller. | |
| # This is just an additional security measure on top of the | |
| # TLS certificate that the ACA app itself has (for normal TLS) | |
| # and the fact that we have restricted the ingress to accept | |
| # traffic only from CloudFlare's IP addresses. | |
| resource "cloudflare_zone_setting" "app_apex_domain" { | |
| zone_id = var.cloudflare_zone_id | |
| setting_id = "tls_client_auth" | |
| value = "on" | |
| } | |
| # ENABLE RATE LIMITING ON THE ZONE | |
| ####################################################################### | |
| # TODO: Put this in the environment setup documentation and that | |
| # if such a rule doesn't exist, and you get an error, then | |
| # you would need to create one in the UI with name "default" first. | |
| # Also note there that: | |
| # | |
| # 1. this rule applies to whole zone as under the free plan, | |
| # you can't set rate limiting rules by hostname. | |
| # But beware that terraform destroy would be removing it for | |
| # the whole zone as well. | |
| # | |
| # 2. The rule of "AUthenticate Origin Pulls" where CLoudFLare | |
| # presents its own cert to the ACA app also applies to the whole | |
| # zone. This we are leaving in. WE SHOULD REALLY BE | |
| # REMOVING THIS AS WELL, TO BE CONSISTENT WITH (1) ABOVE | |
| # | |
| # 3. The zone for the apex domani must already exist. Zone ID | |
| # for that is passed in as cvalue of the cloudflare_zone_id | |
| # variable. | |
| # In the free plan, this applies to all CNAME and A records in the | |
| # zone that are proxied through cloudflare. If you upgrade, you can | |
| # specify different rate limiting rules for hosts in your DNS records | |
| # | |
| # I generated this by creating the rule in the UI. They display | |
| # API Call at the bottom of the page. I took it and tweaked | |
| # it to be properties of this cloudflare_ruleset resource | |
| # | |
| # It still didn't work because a previously deleted rate limiting | |
| # rule, named "default" still existed. All that creating or | |
| # deleting a rate limiting rule for the zone in Cloudflare Dashboard | |
| # was doing was turning it on or off. | |
| # | |
| # This may be because there's | |
| # a limit of 1 rate limiting rule in the free account, but I am | |
| # not sure sure if there wouldn't already be a rate limiting. | |
| # | |
| # So I executed the following cURL, which returned all ruleset. | |
| # From this I picked out the "name" of the ruleset for | |
| # phase = "http_ratelimit" and specified its name - "default" - in | |
| # the name field of the ruleset. | |
| # | |
| # curl -X GET "https://api.cloudflare.com/client/v4/zones/{zone_id}/rulesets" \ | |
| # -H "Authorization: Bearer {api_token}" \ | |
| # -H "Content-Type: application/json" | |
| # | |
| # Contrary to what the documentation for this Terraform resoruce | |
| # suggested (at version 5.6.0), I would not set "id" of that | |
| # ruleset to the "id" that I saw in the returned results. When I | |
| # tried to do that, and got the error that the provider doesn't allow | |
| # "id" to be set and it is a read-only field. | |
| resource "cloudflare_ruleset" "zone_rate_limit" { | |
| zone_id = var.cloudflare_zone_id | |
| name = "default" | |
| # the only kind allowed in free plan | |
| kind = "zone" | |
| phase = "http_ratelimit" | |
| rules = [{ | |
| # We need to set this. | |
| # But I have verified that it still deltes the rate limiting | |
| # rule in the Cloudflare Dashboard on terraform destroy | |
| # (although as described in the comment above, deletion in UI | |
| # just means disablement of the rule named "default") | |
| enabled = true | |
| # Dashboard UI generated this expressions. It means | |
| # reqeusts to any path in the zone are counted | |
| # towards the rate limiting threshold | |
| expression = "(http.request.uri.path wildcard \"/*\")" | |
| action = "block" | |
| ratelimit = { | |
| characteristics = [ | |
| # This just has to be there, don't know why, otherwise | |
| # we get an error on terraform apply | |
| "cf.colo.id", | |
| # I believe that this means that number of requests | |
| # will be evaluated for the same source IP, i.e. | |
| # if the same IP sends a number of requests specified | |
| # by `requests_per_period` argument below, then that | |
| # ip would be blocked. | |
| # In free account, this is one of the few options available. | |
| "ip.src" | |
| ] | |
| # Defines if ratelimit counting is only done when an origin | |
| # is reached. I am taking setting it to false to mean that | |
| # request reaching cloudflare but that were not proxied | |
| # would still count towards the rate limit. | |
| requests_to_origin = false | |
| # This is the number of requests in perion specified by `period` | |
| # property that, if received, would trigger block on subsequent | |
| # requests for a period specified by `mitigation_timeout` property. | |
| requests_per_period = var.rate_limit_requests_per_period | |
| # Over how many seconds should the specified number of requests | |
| # (specified in requests_per_period above) should be received | |
| # or exceeded for the block to be put in place. | |
| # 10 is the only allowable value in free plan | |
| period = 10 | |
| # How many seconds the block will stay in place for | |
| # 10 is the only allowable value in free plan | |
| mitigation_timeout = 10 | |
| } | |
| }] | |
| } | |
| # PROCESS IMPLEMENTED BELOW: | |
| ###################################################### | |
| # If source (domain name) or target (ACA app's FQDN) | |
| # of the CNAME change - or if this is the the first | |
| # terraform apply of the config - we need to go through | |
| # the following steps: | |
| # | |
| # 1. Wait for the chagnes to propagate. | |
| # 2. Set 'proxied=off' on the CNAME record. Otherwise | |
| # managed cert creation and almost certinly the | |
| # custom domain binding creation and update would fail. | |
| # 3. recreate the manager cert | |
| # 4. in order to recerate the cert, we need to destroy | |
| # any existing custom domain binding, then recreate is | |
| # as Disabled | |
| # 5. Once managed cert has been created, we need to | |
| # update the custom domain binding with the cert. | |
| # 6. Set proxied back on the CNAME record. | |
| # | |
| # PROCESS IMPLEMENATTION NOTE: | |
| # In implementing the provess, I have borne in mind the | |
| # details given in document | |
| # [Terraform Core Resource Destruction Notes](https://github.com/hashicorp/terraform/blob/main/docs/destroying.md) | |
| # This data resource to collect the key attributes of CNAME | |
| # record that, if they change, should trigger a replace | |
| # of the key recources that follow whose purpose is to | |
| # suppor the above process | |
| resource "terraform_data" "cname_and_txt_info_for_triggering_replacement" { | |
| input = { | |
| targetfqdn = cloudflare_dns_record.app_cname.content | |
| sourcefqdn = var.app_domain_name | |
| custom_domain_verification_id = local.custom_domain_verification_id | |
| } | |
| } | |
| # First we wait for the DNS records to have propagated | |
| resource "time_sleep" "cname_and_txt_propagated" { | |
| create_duration = "60s" # 60 seconds always seems to work with CloudFlare | |
| depends_on = [cloudflare_dns_record.app_cname, cloudflare_dns_record.app_txt] | |
| # this argument, which predates replace_triggered_by lifecycle property, | |
| # requries a map of values rather than resources. So we set it to | |
| # .output of the terraform_data resoruce rather than the resource itself. | |
| triggers = terraform_data.cname_and_txt_info_for_triggering_replacement.output | |
| } | |
| # Set proxied=false on the CNAME record. | |
| # | |
| # NOTE: | |
| # It would probably have been enough to set proxied=false in CNAME | |
| # record resource with lifecycle.ignore_changes = ["proxied"]. | |
| # The problem with that is that if for any reason the cert or | |
| # custom domain binding had to change or be recreated (e.g, | |
| # we change the domain name that is the source of CNAME record, | |
| # or the ACAP app is replaced as that would generate a new FQDN | |
| # to set as content - i.e. target - of the CNAME record), then | |
| # managed cert would need to be regenerated and custom domain | |
| # binding updated which would fail because the last time round | |
| # we had set `proxied==true` right at the end of the process | |
| # when managed cert had been created and custom domain binding | |
| # updated. | |
| # | |
| # We would still set proxied=true in that way (at the end | |
| # of the process), but we now we explicitly turn it to off | |
| # just after updates to key data in cname or txt records | |
| # (domain name, target of CNAME record, or TXT value) have | |
| # occurred and propagated, and just before any updates to | |
| # mnaged cert and custom domain binding may need to happen | |
| # as a result. | |
| resource "restful_operation" "turn_off_proxied_in_cname_record" { | |
| depends_on = [time_sleep.cname_and_txt_propagated] | |
| lifecycle { | |
| replace_triggered_by = [terraform_data.cname_and_txt_info_for_triggering_replacement] | |
| } | |
| provider = restful.cloudflare | |
| path = "/zones/${cloudflare_dns_record.app_cname.zone_id}/dns_records/${cloudflare_dns_record.app_cname.id}" | |
| method = "PATCH" | |
| body = { "proxied" : false } | |
| # I think we can also poll for retry. See resource | |
| # documentation in terraform registry (under | |
| # magodo/restful provider) | |
| } | |
| # Initialize a custom domain binding in the ingress of the app | |
| # We can do it now, but couldn't when we created the app, because | |
| # now the DNS records with CloudFlare would have propagated. Those | |
| # would be validated by Azure when it creates the binding. | |
| resource "azapi_resource_action" "custom_domain_binding_initialize" { | |
| depends_on = [time_sleep.cname_and_txt_propagated, restful_operation.turn_off_proxied_in_cname_record] | |
| lifecycle { | |
| # This resource does nothing on update or destroy. | |
| # But we do want it to be re-created if key bits in CNAME | |
| # or TXT change. Hence setting replace_triggered_by below. | |
| # | |
| # During replacement, the actual destruction of the custom | |
| # binding that this resource creates/initialisez is achieved by | |
| # resource azapi_resource_action.custom_domain_binding_destroy | |
| # which has been configured to ONLY operate during destroys and | |
| # deletes the very custom binding that resource below initializes. | |
| replace_triggered_by = [terraform_data.cname_and_txt_info_for_triggering_replacement] | |
| } | |
| type = "Microsoft.App/containerApps@2023-05-01" | |
| resource_id = azurerm_container_app.app.id | |
| # It seems `action` just gets appended to the URL obtained by | |
| # appending `resource_id` to the base url for the `type` | |
| # this gives us URLs for actions on some resources like | |
| # `/start` on a Web App in an App Service Plan. | |
| # WE DON't WANT TO USE IT HERE | |
| # | |
| #action = "" | |
| # default for this resource is POST, unlike for the | |
| # azapi_update_resource for which it seems to be PATCH | |
| # from my Fiddler investigations | |
| method = "PATCH" | |
| body = { | |
| properties = { | |
| configuration = { | |
| ingress = { | |
| customDomains = [ | |
| { | |
| bindingType = "Disabled", | |
| name = var.app_domain_name, | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| } | |
| } | |
| # azurerm can't create a managed TLS certificate - | |
| # see https://github.com/hashicorp/terraform-provider-azurerm/issues/21866 | |
| # | |
| # So instead of using azurerm provider the following resources are | |
| # created using AzAPI provider to make directy API calls to Azure. | |
| resource "azapi_resource" "managed_certificate" { | |
| depends_on = [azapi_resource_action.custom_domain_binding_initialize, time_sleep.cname_and_txt_propagated, cloudflare_dns_record.app_cname, cloudflare_dns_record.app_txt, restful_operation.turn_off_proxied_in_cname_record] | |
| # Unlike azapi_update_resource and azapi_resource_action, that we have used in this | |
| # process to patch the custom domain binding on the ingress of the ACA app | |
| # this resource is fully managed through create update and destroy. However, | |
| # an actual managed cert needs repalcement when the domain name or the FQDN | |
| # of the backing ACA app changes. Hence lifecycle.repalce_triggered_by below | |
| # is the same as that for the other azapi resources and restful_operation | |
| # resources used in this process. | |
| lifecycle { | |
| replace_triggered_by = [terraform_data.cname_and_txt_info_for_triggering_replacement] | |
| } | |
| type = "Microsoft.App/managedEnvironments/managedCertificates@2023-05-01" | |
| name = "${lower(var.app_name)}-cert" | |
| parent_id = azurerm_container_app_environment.app.id | |
| location = azurerm_resource_group.app.location | |
| body = { | |
| properties = { | |
| subjectName = var.app_domain_name | |
| domainControlValidation = "CNAME" | |
| } | |
| } | |
| response_export_values = ["*"] | |
| } | |
| # update the already-create custom domain binding in the app's ingress | |
| # with the certificate and make the binding enabled. | |
| resource "azapi_update_resource" "custom_domain_binding_update" { | |
| depends_on = [restful_operation.turn_off_proxied_in_cname_record, azapi_resource_action.custom_domain_binding_initialize, azapi_resource.managed_certificate] | |
| type = "Microsoft.App/containerApps@2023-05-01" | |
| resource_id = azurerm_container_app.app.id | |
| lifecycle { | |
| # this resource only operates during create and update, not destroy | |
| # we do want the underlying domain binding to be destroyed when | |
| # this resource needs to be recreated. That is acheived by the | |
| # azapi_resource_action.custom_domain_binding_destroy resource | |
| # which would run only on destroy and deleted the custom domain | |
| # binding being updated by this resource. | |
| replace_triggered_by = [terraform_data.cname_and_txt_info_for_triggering_replacement] | |
| } | |
| body = { | |
| properties = { | |
| configuration = { | |
| ingress = { | |
| customDomains = [ | |
| { | |
| bindingType = "SniEnabled", | |
| name = var.app_domain_name, | |
| certificateId = azapi_resource.managed_certificate.output.id | |
| } | |
| ] | |
| } | |
| } | |
| } | |
| } | |
| } | |
| # The azapiupdate_resource that creates custom binding above works | |
| # during create and update phases of the lifecycle. | |
| # However, it does nothing during destroy. This leads to a problem | |
| # when destroying the managed cert with the error that the cert | |
| # is in use in a custom binding (which has not beeen destroyed). | |
| # | |
| # This resource does nothing during update and, depending on setting | |
| # of 'when' argument, works eother during create, or during destroy. | |
| # We have set it to run during destroy so it destroys the | |
| # custom domain binding, so that the managed cert can | |
| # then be destroyed or the cusomt domain binding re-created by | |
| # the recreation of custim_domain_binding_initialize and | |
| # custom_domain_binding_update resources above in the right order. | |
| resource "azapi_resource_action" "custom_domain_binding_destroy" { | |
| type = "Microsoft.App/containerApps@2023-05-01" | |
| lifecycle { | |
| replace_triggered_by = [terraform_data.cname_and_txt_info_for_triggering_replacement] | |
| } | |
| # following line also makes this resource a dependent | |
| # on the custom_domain_binding_create resource. | |
| resource_id = azapi_update_resource.custom_domain_binding_update.resource_id | |
| method = "PATCH" | |
| response_export_values = ["*"] | |
| body = { | |
| properties = { | |
| configuration = { | |
| ingress = { | |
| customDomains = [] # ensures the custom domain would be deleted | |
| } | |
| } | |
| } | |
| } | |
| when = "destroy" | |
| } | |
| resource "restful_operation" "turn_on_proxied_in_cname_record" { | |
| depends_on = [azapi_update_resource.custom_domain_binding_update, azapi_resource.managed_certificate, | |
| # We know that resource named below would | |
| # definitely be replaced for the same reasons as this one is. | |
| # However, we want to be certain that the the present resource | |
| # would run to turn proxied back on definitely after it has | |
| # been turned off. Hence the following dependency. | |
| restful_operation.turn_off_proxied_in_cname_record] | |
| lifecycle { | |
| replace_triggered_by = [terraform_data.cname_and_txt_info_for_triggering_replacement] | |
| } | |
| provider = restful.cloudflare | |
| path = "/zones/${cloudflare_dns_record.app_cname.zone_id}/dns_records/${cloudflare_dns_record.app_cname.id}" | |
| method = "PATCH" | |
| body = { "proxied" : true } | |
| # I think we can also poll for retry. See resource | |
| # documentation in terraform registry (under | |
| # magodo/restful provider) | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| terraform { | |
| required_providers { | |
| azurerm = { | |
| # Following environment variables must be provided for this provider: | |
| # ARM_SUBSCRIPTION_ID, ARM_CLIENT_ID, ARM_TENANT_ID, ARM_CLIENT_SECRET | |
| # | |
| # All of these - except ARM_SUBSCRIPTION_ID can be obtained when | |
| # you create a service principle in Entra in Azure portal. | |
| # | |
| # For ARM_SUBSCRIPTION_ID, provide the ID of a subscription | |
| # in your Azure account. | |
| source = "hashicorp/azurerm" | |
| version = "4.34.0" # Pinned to an exact version for repeatabilityneeded | |
| } | |
| cloudflare = { | |
| source = "cloudflare/cloudflare" | |
| version = "5.6.0" # pinned to exact version for repeatability | |
| } | |
| azapi = { | |
| source = "azure/azapi" | |
| version = "2.4.0" # pinned to exact version for repeatability | |
| } | |
| time = { | |
| source = "hashicorp/time" | |
| version = "0.13.1" # pinned version for repeatability | |
| } | |
| # restapi = { | |
| # source = "Mastercard/restapi" | |
| # version = "2.0.1" # version pinned for repeatability | |
| # } | |
| restful = { | |
| source = "magodo/restful" | |
| version = "0.22.0" # version pinned for repeatability | |
| } | |
| } | |
| } | |
| provider "azurerm" { | |
| features { | |
| resource_group { | |
| prevent_deletion_if_contains_resources = false | |
| } | |
| } | |
| } | |
| provider "azapi" { | |
| # this needs to the same four environment | |
| # variables to be provided that we are setting | |
| # for the azurerm provider above | |
| } | |
| provider "cloudflare" { | |
| api_token = var.cloudflare_api_token | |
| } | |
| provider "time" { | |
| } | |
| provider "restful" { | |
| alias = "cloudflare" | |
| # Configuration options | |
| base_url = "https://api.cloudflare.com/client/v4" | |
| header = { | |
| Content-Type = "application/json" | |
| Authorization = "Bearer ${var.cloudflare_api_token}" | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| variable "app_resource_group_name" { | |
| description = "The name of the Azure resource group in which the app would be created." | |
| type = string | |
| } | |
| variable "app_resource_group_location" { | |
| description = "Location of the Azure resource group n which the app would be created." | |
| type = string | |
| } | |
| variable "allowed_cors_origins_for_api" { | |
| description = "the string that would be set as value of config key ALLOWED_CORS_ORIGINS for the API" | |
| type = string | |
| } | |
| variable "core_resource_group_name" { | |
| description = "The name of the Azure resource group that contains supporting resources such as key vault, ACR etc. this would already have been created by a different module/workspace." | |
| type = string | |
| } | |
| variable "app_name" { | |
| description = "Name of the ACA app that would be created." | |
| type = string | |
| } | |
| variable "app_environment_name" { | |
| description = "Name of the ACA environment that would be created and would contain the ACA app" | |
| type = string | |
| } | |
| variable "flowmazon_api_managed_identity" { | |
| description = "Name of the user-assigned managed identity that would be assigned to the ACA app" | |
| type = string | |
| } | |
| variable "app_key_vault_name" { | |
| description = "The name of the key vault from which secrets required by the app will be read." | |
| type = string | |
| } | |
| variable "key_vault_secretname_connectionstring_for_api" { | |
| description = "name of the secret whose value is the connection string to be used by the API to connect to the database" | |
| type = string | |
| } | |
| variable "app_domain_name" { | |
| description = "The custom domain name for the app, e.g. api.efast.uk" | |
| type = string | |
| } | |
| variable "app_container_name" { | |
| description = "Name of the container that would be created in the ACA app" | |
| type = string | |
| } | |
| variable "app_container_port" { | |
| description = "port at which the app container listens" | |
| type = number | |
| # default = 80 | |
| } | |
| variable "acr_name" { | |
| description = "The name of the Azure Ccontainer Registry instance which contains the image to be deployed to the ACA app." | |
| type = string | |
| } | |
| variable "image_repository_name" { | |
| description = "Name of the Docker image to deploy (excluding the '<registry name>.azurecr.io/' prefix and the ':<tag>' suffix)" | |
| type = string | |
| } | |
| variable "version_to_deploy" { | |
| description = "SemVer version to deploy, starting with 'v'. this would be the tag of the image for flowmazonapi in the container registry e.g. `v1.0.1`" | |
| type = string | |
| } | |
| variable "app_revision_mode" { | |
| description = "revision mode of the container app. Multiple or Single" | |
| type = string | |
| # # better practice for revision mode to be "Multiple" | |
| # # but "Single" is a simpler default | |
| # default = "Single" | |
| } | |
| locals { | |
| allowed_cors_origins_env_var_name = "ALLOWED_CORS_ORIGINS" | |
| connection_string_env_var_name = "ConnectionStrings__FlowmazonDB" | |
| } | |
| # Cloudflare variables | |
| # To generate this token, create an Account API token by going | |
| # to your user profile in your CloudFalre account, then clicking | |
| # **API Token** in the nav on the left hand side. | |
| # | |
| # Note an Account API token is preferred to a User API token. | |
| # | |
| # Give the token the following permissions: | |
| # 1. 'Zone | DNS | Edit` (to create and modify TXT and CNAME records) | |
| # 2. 'Zone | Zone Settings | Edit' (to set 'tls_client_auth' setting | |
| # 3. 'Zone | Zone WAF | Edit` (to set rate limiting rule) | |
| # to 'on' which ensures that CloudFlare would present a certificate | |
| # to target of CNAME record when communicating with it; this | |
| # achieves mTLS. | |
| # | |
| # Make sure that the zone of interest on which these permissions apply | |
| # is also selected under 'Zone Resources' - or 'All zones from the acount' | |
| # is selected - when you assign permissions to the token. | |
| # | |
| variable "cloudflare_api_token" { | |
| description = "CloudFlare's API Token." | |
| type = string | |
| sensitive = true | |
| } | |
| # TODO: document this in my environments documentation | |
| # | |
| # I assume you already have a DNS zone with CloudFlare for | |
| # the apex domain used in the domain name of the app. For example | |
| # if the domain name if `api.efast.uk`, the apex domain is | |
| # efast.uk. | |
| # | |
| # Create this zone by transferring setting Cloudflare's nameservers | |
| # as nameservers of the apex domain in control panel of the registrar | |
| # from whom you purchased the apex domain. | |
| # Cloudflare would also guide you through the process. | |
| # Once you have created an zone, i.e. Cloudflare manage | |
| # DNS queries for the apex domain, then go to CloudFlare Dashboard. | |
| # There click the apex domain name, and you would be taken to | |
| # the detail page of the associated zone. ZoneID would be | |
| # displayed on this page (you may have to scroll down the page). | |
| variable "cloudflare_zone_id" { | |
| description = "Zone ID of the apex domain for domain name specified in api_domain_name variable." | |
| type = string | |
| sensitive = true | |
| } | |
| variable "rate_limit_requests_per_period" { | |
| description = "number of request that, if received in the configured `period` (fixed to 10 seconds in free plan), would lead to the sending IP being blocked for the configured `mitigation_timeout` (also fixed to 10 seconds in free plan)" | |
| type = number | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment