Deploy Azure Communication Services (ACS) with Terraform

Azure Communication Services (ACS) is an API communication service inside Azure that can help you send voice, video, chat, text messaging/SMS, email and more. In this guide, I’ll walk you through the steps to set up Azure Communication Services (ACS) with a custom domain using Terraform. This process involves configuring Azure DNS, verifying your domain, and ensuring your domain is properly linked to ACS for seamless email communication.

Pre-requisites

  • An Azure account
  • Terraform installed on your local machine
  • A code editor such as Visual Studio Code
  • An Azure DNS zone deployed for an external domain name

To be able to send emails from Azure Communications Services (ACS), you need to associate a domain. You can select one that Azure spins up for you, or you can use an existing one you have hosted within an Azure DNS zone or another DNS provider.  For this Terraform example, I am using an existing domain I have, which is hosted within an Azure DNS zone. You can, of course, modify the code to suit your needs. 

Terraform configuration

I have split my Terraform configuration to deploy Azure Communication Services (ACS) into three files: 

  • Main.tf: This holds my resource configuration. 
  • Variables.tf: This holds any variables I need to populate and pull into my main.tf file.
  • Providers.tf: This lists my Terraform configuration and the providers I am using to deploy my resources.

Below is my providers.tf file:

##
# Terraform Configuration
##

terraform {
  required_version = ">= 1.10.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.71, < 5.0.0"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.5.1, < 4.0.0"
    }
    azapi = {
      source  = "Azure/azapi"
      version = ">= 2.2.0, < 3.0.0"
    }
  }
}

provider "azapi" {
  # Configuration options
}

provider "azurerm" {
  features {}
  subscription_id = "xxx-xxx-xxx-xxx"
}

Below is my variables file:

##
# Variables
##

##
# Common Variables
##
variable "tag_environment" {
  type    = string
  default = "Testing"
}

variable "tag_project" {
  type    = string
  default = "AzureCommunicationServices"
}

variable "tag_creator" {
  type    = string
  default = "TechieLass"
}


##
# Domain Variables
##

variable "ttl_setting" {
  description = "The TTL setting for the DNS records"
  type        = number
  default     = 3600
}

variable "custom_domain" {
  description = "The custom domain for the email communication service"
  type        = string
  default     = "ctrlaltscots.info"
}

variable "custom_domain_rg" {
  description = "The resource group for the custom domain"
  type        = string
  default     = "rg-lzpt"
}

##
#  Communication Variables
##

variable "acs_name" {
  description = "Name of the ACS service to be deployed."
  type        = string
  default     = "techielass-acs"
}

variable "data_location" {
  description = "The location of the email data."
  type        = string
  default     = "UK" # Or use Africa, Asia Pacific, Australia, Brazil, Canada, Europe, France, Germany, India, Japan, Korea, Norway, Switzerland, UAE, UK, usgov or United States as needed.
}

Below is my main.tf file:


# This ensures we have unique CAF compliant names for our resources.
module "naming" {
  source  = "Azure/naming/azurerm"
  version = "0.3.0"
}

locals {
  azure_regions = [
    "ukwest",
    "westeurope",
    "francecentral",
    "swedencentral"
    # Add other regions as needed
  ]
}

# This picks a random region from the list of regions.
resource "random_integer" "region_index" {
  max = length(local.azure_regions) - 1
  min = 0
}

# This is required for resource modules
resource "azurerm_resource_group" "rg" {
  location = local.azure_regions[random_integer.region_index.result]
  name     = module.naming.resource_group.name_unique

  tags = {
    Environment = var.tag_environment
    Project     = var.tag_project
    Creator     = var.tag_creator
  }
}

resource "azurerm_communication_service" "acs" {
  name                = var.acs_name
  resource_group_name = azurerm_resource_group.rg.name
  data_location       = var.data_location
}

resource "azurerm_email_communication_service" "acsemail" {
  name                = "${module.naming.resource_group.name_unique}-email"
  resource_group_name = azurerm_resource_group.rg.name
  data_location       = var.data_location
}

resource "azurerm_email_communication_service_domain" "acsdomain" {
  name             = var.custom_domain
  email_service_id = azurerm_email_communication_service.acsemail.id

  domain_management = "CustomerManaged"
}

# Create a TXT record for domain verification in the DNS zone
resource "azurerm_dns_txt_record" "domain" {
  count               = 1
  name                = "@"
  zone_name           = var.custom_domain
  resource_group_name = var.custom_domain_rg
  ttl                 = var.ttl_setting

  # Add the TXT record for domain verification
  record {
    value = element(azurerm_email_communication_service_domain.acsdomain.verification_records[0].domain[*].value, count.index)
  }

  # Add record for SPF Verification
  record {
    value = element(azurerm_email_communication_service_domain.acsdomain.verification_records[0].spf[*].value, count.index)
  }

  # Ensure the ACS Email service & Azure DNS serviec has been deployed before creating these records
  depends_on = [azurerm_email_communication_service_domain.acsdomain]
}

# Create a CNAME record for DKIM verification in the DNS zone
resource "azurerm_dns_cname_record" "dkim" {
  count               = 1
  name                = element(azurerm_email_communication_service_domain.acsdomain.verification_records[0].dkim[*].name, count.index)
  zone_name           = var.custom_domain
  resource_group_name = var.custom_domain_rg
  ttl                 = element(azurerm_email_communication_service_domain.acsdomain.verification_records[0].dkim[*].ttl, count.index)
  record              = element(azurerm_email_communication_service_domain.acsdomain.verification_records[0].dkim[*].value, count.index)

  # Ensure the ACS Email service & Azure DNS serviec has been deployed before creating these records
  depends_on = [azurerm_email_communication_service_domain.acsdomain]
}

# Create a CNAME record for DKIM2 verification in the DNS zone
resource "azurerm_dns_cname_record" "dkim2" {
  count               = 1
  name                = element(azurerm_email_communication_service_domain.acsdomain.verification_records[0].dkim2[*].name, count.index)
  zone_name           = var.custom_domain
  resource_group_name = var.custom_domain_rg
  ttl                 = element(azurerm_email_communication_service_domain.acsdomain.verification_records[0].dkim2[*].ttl, count.index)
  record              = element(azurerm_email_communication_service_domain.acsdomain.verification_records[0].dkim2[*].value, count.index)

  # Ensure the ACS Email service & Azure DNS serviec has been deployed before creating these records
  depends_on = [azurerm_email_communication_service_domain.acsdomain]
}

# Initiate Domain Verification
# API: https://learn.microsoft.com/en-us/rest/api/communication/resourcemanager/domains/initiate-verification?view=rest-communication-resourcemanager-2023-03-31&tabs=HTTP
resource "azapi_resource_action" "validate_domain" {
  count       = 1
  type        = "Microsoft.Communication/emailServices/domains@2023-03-31"
  action      = "initiateVerification"
  resource_id = azurerm_email_communication_service_domain.acsdomain.id

  body = {
    verificationType = "Domain" # or use "SPF", "DKIM", "DMARC", "DKIM2" as needed
  }
  depends_on = [azurerm_dns_txt_record.domain]
}

# Initiate SPF Verification
# API: https://learn.microsoft.com/en-us/rest/api/communication/resourcemanager/domains/initiate-verification?view=rest-communication-resourcemanager-2023-03-31&tabs=HTTP
resource "azapi_resource_action" "validate_spf" {
  count       = 1
  type        = "Microsoft.Communication/emailServices/domains@2023-03-31"
  action      = "initiateVerification"
  resource_id = azurerm_email_communication_service_domain.acsdomain.id

  body = {
    verificationType = "SPF" # or use "SPF", "DKIM", "DMARC", "DKIM2" as needed
  }
  depends_on = [azapi_resource_action.validate_domain]
}

# Initiate DKIM Verification
# API: https://learn.microsoft.com/en-us/rest/api/communication/resourcemanager/domains/initiate-verification?view=rest-communication-resourcemanager-2023-03-31&tabs=HTTP
resource "azapi_resource_action" "validate_dkim" {
  count       = 1
  type        = "Microsoft.Communication/emailServices/domains@2023-03-31"
  action      = "initiateVerification"
  resource_id = azurerm_email_communication_service_domain.acsdomain.id

  body = {
    verificationType = "DKIM" # or use "SPF", "DKIM", "DMARC", "DKIM2" as needed
  }
  depends_on = [azapi_resource_action.validate_spf]
}

# Initiate DKIM2 Verification
# API: https://learn.microsoft.com/en-us/rest/api/communication/resourcemanager/domains/initiate-verification?view=rest-communication-resourcemanager-2023-03-31&tabs=HTTP
resource "azapi_resource_action" "validate_dkim2" {
  count       = 1
  type        = "Microsoft.Communication/emailServices/domains@2023-03-31"
  action      = "initiateVerification"
  resource_id = azurerm_email_communication_service_domain.acsdomain.id

  body = {
    verificationType = "DKIM2" # or use "SPF", "DKIM", "DMARC", "DKIM2" as needed
  }
  depends_on = [azapi_resource_action.validate_dkim]
}

# Association of the Email Domain with the Communication Service
resource "azurerm_communication_service_email_domain_association" "email_domain_association" {
  communication_service_id = azurerm_communication_service.acs.id
  email_service_domain_id  = azurerm_email_communication_service_domain.acsdomain.id
  depends_on               = [azapi_resource_action.validate_dkim2]
}

All of these files are stored within one directory. You can find a copy of the code on GitHub here: https://github.com/weeyin83/terraform-acs-deployment-private/tree/main/acs-deployment

Terraform configuration files explained

Let’s explore why I’ve got all the various lines within my files. 

Providers.tf


In my provider.tf file I have a terraform block which defines the required Terraform version and providers that I need for this deployment. 

I’ve specified that I want to use Terraform version 1.10.0 or newer.  I’ve said I need the azurerm provider, which is the Azure provider and helps to manage Azure resources. I’ve said I want to use version 3.71 or newer but below 5.0. 

I’ve also said I want to use the random provider, which is used to generate random values, and I’ve said I want to use version 3.5.1 or newer but below 4.0. 

And I’ve also said I need to use the azapi provider, which allows me to access the Azure API for deploying resources. And I’ve said I want to use version 2.2.0 or newer but below 3.0.  

Within the provider block of this file, I have not specified any configuration options for the azapi provider, but I have specified my Azure subscription ID within the azurerm provider block, as that is necessary.

Variables.tf

Within the variables file, I have included information I need to input into my main Terraform file to help with deployment.  I’ve split it up into three sections: variables that are generic, variables that relate to the existing Azure DNS zone that is deployed and then variables that will be used to create the new Azure Communication Services resources. 

Within the “Comm Variables” section, I have defined tags that are applied to the resource group to help me identify what environment this is for, the project the resources support and the creator.  You can change the tag names, uses and definition to suit how you tag your resources. 

Within the “Domain Variables” section we have:

  • ttl_setting: This variable defines the TTL (Time to Live) setting for the DNS records. It's a number and has a default value of 3600 seconds (1 hour).
  • custom_domain: This variable is for specifying the custom domain to be used by the email communication service.
  • custom_domain_rg: Specifies the Azure resource group where my Azure DNS zone is deployed. 

Within the “Communication Variables” section we have: 

  • acs_name: This defines the name of the Azure Communication Services (ACS) service that will be deployed. 
  • data_location: This variable sets the location of the email data. You can specify Africa, Asia Pacific, Australia, Brazil, Canada, Europe, France, Germany, India, Japan, Korea, Norway, Switzerland, UAE, UK, usgov or United States as needed.

You can modify this file to match your requirements. 

Main.tf

This is the Terraform configuration deployment file.  

In the first section, I am calling the Azure naming module and have defined a local variable block.  By calling the Azure naming module I ensure that resources follow an Azure Cloud Adoption Framework (CAF) compliant naming convention where possible.   My local variable section defines which Azure regions I am happy for the resources to be created in and later on I use a random_integer resource to select which one randomly.

💡
You can find which Azure resources can use the naming module to create CAF compliant names within the official documentation here: Azure/naming/azurerm | Terraform Registry

The next part of the file is in place to create the resource group that we will ultimately deploy our resources into.  The region is randomly selected, tags are applied and the name is created using the naming module. 

In the next section we’re starting to create the Azure Communication Services components.  We first create the Azure Communication Services resource itself, and then we deploy the email service that’s associated with the ACS resource. 

As part of this deployment, I also associated the existing Azure DNS domain to the email services component. 

The next thing that Terraform creates as part of this deployment is several DNS records to verify the domain and configure email authentication for Azure Communication Services (ACS). These records ensure ACS can send emails on behalf of your domain while maintaining security and compliance. The key DNS records are:

  • TXT Record (Domain Verification) – Confirms domain ownership by adding a Microsoft-provided token to DNS. ACS checks this record to verify control over the domain.
  • SPF Record (Sender Policy Framework) – Specifies authorised mail servers to prevent spoofing. Without this, emails sent via ACS might be flagged as spam.
  • CNAME Records (DKIM - DomainKeys Identified Mail) – Enables cryptographic signing of emails to verify authenticity and prevent tampering. ACS requires two CNAME records pointing to Microsoft’s DKIM servers.

Once the required DNS records are created, we need to ensure they are correctly set up and accessible. Azure Communication Services (ACS) requires verification of these records before the email service can function properly. This Terraform configuration triggers the verification process using Azure’s API.

Each azapi_resource_action block calls the initiateVerification API to verify our TXT, SPF and CNAME records. 

Also written into the Terraform are depends_on statements to help enforce the correct order of execution, ensuring that each step completes before moving to the next. Without this verification step, ACS cannot confirm domain ownership, and emails sent through the service may fail authentication or be marked as spam.

The final step in the deployment is to formally link the verified domain to the Azure Communication Services (ACS) email service. While we’ve already created DNS records and verified them, ACS still needs to recognize the domain as an authorized sender for email communications.  Without this step, ACS would not be able to send emails from the configured domain, as the domain would not be recognized as a valid sender.

By completing this association, ACS can securely and reliably send emails on behalf of your domain, ensuring compliance with authentication standards like SPF, DKIM, and DMARC.

And this completes the deployment configuration. If you deploy this via Terraform you will end up with resources like below.

Deployment

You can now take this Terraform code and deploy it using your usual Terraform workflow. If you need guidance on deploying Terraform using CI/CD tools, check out my guides:

If you're new to Terraform and need a beginner-friendly guide on deploying Azure resources, you might find this helpful: 

Conclusion

This Terraform deployment enables you to provision Azure Communication Services (ACS) along with its email service and the necessary DNS records. Once deployed, you’ll be able to send emails through the service.

Looking ahead, I plan to explore ways to enhance monitoring and security for ACS. Stay tuned for future posts on these improvements—be sure to subscribe so you don’t miss out!