Terraform an Azure Order Pipeline
Use Terraform to deploy a secure Azure event-driven order pipeline.
Introduction
30 Second Summary
An online order often needs to trigger several jobs at once. If every job shares one copy, the first job can leave nothing for the others.
In this project, you will use Terraform to build a repeatable order pipeline on Microsoft Azure that receives events through Azure Event Grid. Azure Service Bus gives an Azure Function one processing copy while analytics and notifications retain separate copies.
What You'll Build
Publish an order event from Windows PowerShell to watch the Function consume its queue copy before you peek at the independent analytics and notifications copies in the Azure portal.
By the end of this project, you'll have:
- A live order-processing path that turns a published event into a consumed Function work item.
- Independent analytics and notifications copies that you can inspect to demonstrate topic fan-out.
- A portfolio-ready deployment that explains managed identity, Azure RBAC, Azure Key Vault, validation, and Terraform teardown through a Mermaid architecture diagram.
- Secret Mission: Add subject-prefix filtering so only priority orders reach the analytics and notifications subscriptions.
Are there any prerequisites?
This project assumes you are working on Windows with Terraform, Azure CLI, Git, Visual Studio Code, and Windows PowerShell already installed.
You also need an Azure subscription that lets you create resources and role assignments.
Before We Start
Before any hands-on work, commit to an architecture where Event Grid routes each order event to the orders Service Bus queue for one Azure Function processor. A Service Bus topic keeps separate copies for analytics and notifications.
Verify Your Local Setup
A mismatched Terraform version can reject the configuration before deployment. A wrong Microsoft Azure subscription can place billable resources outside your promotional credit.
This step prepares your project workspace in Windows PowerShell. You will confirm Terraform CLI 1.16.5 plus the Azure CLI. You will also authenticate to the subscription that should own the deployment.
In this step, get ready to:
- Prepare the project workspace and planned folder structure.
- Confirm the required command-line tools are available.
- Authenticate to the intended Azure subscription.
Create the project workspace
A workspace gives every future Terraform file the same project context. Visual Studio Code displays that workspace through its Explorer sidebar.
- Press the Windows key to open Windows search.
- Type Visual Studio Code and press Enter to open it.
What is a workspace?
A VS Code workspace is the folder that holds one project. Opening the folder lets the Explorer show its complete structure.
- Select File from the top menu.
- Select Open Folder....
- Select your Desktop in the folder dialog.
- Select New Folder.
- Enter your project folder name.
- Select Select Folder.
- Select Yes, I trust the authors if the Workspace Trust dialog appears.
Why trust this workspace?
Workspace Trust controls whether VS Code can execute project code. You created this empty folder yourself, so you know where its future contents come from.
- Right-click your workspace folder in the Explorer sidebar.
- Select New Folder.
- Enter function.
- Right-click the new function folder.
- Select New Folder.
- Enter OrderProcessor.
The Explorer now shows your project folder name with function/OrderProcessor nested inside it.
How the project stays organized
- The Terraform configuration files will live at the top level of the workspace.
- The architecture documentation will live at README.md.
- The event publisher will live at send-event.ps1.
- The Function host files will live under function. The queue processor files will live under function/OrderProcessor.
Cannot see the nested folders?
Confirm that OrderProcessor appears inside function in the Explorer. Drag it onto function if it was created beside that folder.
Still stuck? Help me fix my VS Code project folder structure.
Verify Terraform and Azure CLI
The project configuration pins Terraform CLI to exactly 1.16.5. Checking the local executable now prevents a version mismatch during initialization.
- Press the Windows key to open Windows search.
- Type Windows PowerShell and press Enter to open it.
- Check the installed Terraform version by running this command:
terraform version
What does this check?
This command reports the Terraform executable that Windows PowerShell finds. The first line needs to show version 1.16.5 for this project's exact version pin.
✔️ I see the required version
Terraform reports version 1.16.5. Your local executable matches the version this project expects.
- Continue to the Azure CLI check below.
ⓧ I see an older or different version
Windows PowerShell is finding another Terraform executable. Replace that executable before you initialize the project.
- Open the official Terraform binaries page for version 1.16.5.
- Download the Windows archive that matches your computer's processor.
- Extract the Terraform executable from the archive.
- Replace the older Terraform executable in the folder listed on your Windows Path.
- Close Windows PowerShell.
- Open a new Windows PowerShell window.
- Repeat the Terraform version check shown above.
ⓧ Command not found
Windows cannot find Terraform on its Path. The Path is the list of folders Windows searches for command-line tools.
- Open the official Terraform binaries page for version 1.16.5.
- Download the Windows archive that matches your computer's processor.
- Extract the Terraform executable into a permanent folder.
- Add that folder to your Windows Path.
- Close Windows PowerShell.
- Open a new Windows PowerShell window.
- Repeat the Terraform version check shown above.
Terraform still reports another version?
Close every open PowerShell window after changing the executable. A new session reloads the Windows Path.
Still seeing the wrong version? Help me find which Terraform executable Windows PowerShell is using.
- Check that Azure CLI is available by running this command:
az --version
What does this check?
This command reports the installed Azure CLI components. Any normal version output confirms that PowerShell can use the CLI for local Azure authentication.
✔️ I see Azure CLI version details
Azure CLI is available in Windows PowerShell. You are ready to authenticate.
- Continue to the subscription sign-in below.
ⓧ Command not found
Windows cannot find Azure CLI. Install it before continuing with subscription authentication.
- Open the official Azure CLI installation guide for Windows.
- Follow the Windows installation steps for WinGet or the Microsoft Installer.
- Close Windows PowerShell after the installation finishes.
- Open a new Windows PowerShell window.
- Repeat the Azure CLI version check shown above.
That is your first deployment guardrail in place. Terraform matches the project's exact pin while Azure CLI is ready to connect your workstation to Azure.
Sign in to the intended Azure subscription
Azure CLI uses your active subscription as the destination for later Terraform operations. The selected subscription must be the one covered by your promotional credit.
Signing in handles your Azure credentials, which can feel like a high-stakes step. This action only authenticates your local CLI and selects an account. It creates no resources or charges.
Windows may open a browser or a Microsoft sign-in window during authentication. Complete that prompt with the account that owns the intended subscription.
- Start interactive Azure authentication by running this command:
az login
What does this command do?
This command starts interactive authentication for Azure CLI. The resulting local session lets later Terraform operations use your Azure account without placing account credentials in project files.
- Complete the Microsoft sign-in prompt with the account that owns your promotional credit.
- Select the intended account if the sign-in flow shows more than one account.
- Choose the subscription covered by your promotional credit if a subscription selector appears.
- Return to Windows PowerShell after authentication completes.
Authentication did not complete?
Confirm that the browser or sign-in window used the same account as your Azure subscription. Repeat the authentication command after signing out of an unintended Microsoft account.
Still blocked? Help me troubleshoot Azure CLI interactive sign-in on Windows.
Before you run the final check, which subscription name do you expect the active session to show?
- Display the active Azure subscription in a readable table by running this command:
az account show --output table
What does this command confirm?
This command reads the active subscription from your authenticated Azure CLI session. The table makes the selected subscription easy to compare with the subscription covered by your promotional credit.
You will see one table row for the active subscription.
- Compare the displayed subscription name with the subscription covered by your promotional credit.
- Confirm that the displayed subscription identifier belongs to the intended subscription.
Seeing the wrong subscription?
Repeat the interactive sign-in command from above. Choose the intended subscription when the subscription selector appears.
Need help choosing the correct subscription? Help me verify which Azure subscription my CLI session should use.
Your local workspace now points at the correct tools and Azure subscription. Next, you will turn that empty folder into a repeatable Terraform deployment for the secure queue foundation.
Deploy the Secure Queue Foundation
The subscription choice from the last step gives Terraform a safe target. You are still signed in to the intended Azure subscription.
That subscription has no repeatable processing path yet. This step creates the smallest secure foundation for routing an order event into an Azure Service Bus queue through Azure Event Grid.
Pinned versions keep the deployment reproducible before billable resources appear. Terraform state then records the infrastructure that Terraform must update or remove.
In this step, get ready to:
- Create the Terraform configuration with pinned providers.
- Define the secure queue foundation with managed identities.
- Deploy the foundation to your selected Azure subscription.
Create the Terraform guardrails
The configuration starts with version constraints and input variables. A .gitignore file also keeps local Terraform state out of Git.
- Create a file named .gitignore in the open VS Code project folder.
- Add these exclusions to .gitignore by pasting the code below:
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
terraform.tfvars
function.zip
crash.log
What does this file protect?
- The state patterns keep Terraform's local infrastructure records out of Git.
- The variable file exclusion protects your subscription-specific configuration.
- The ZIP exclusion keeps the generated Function package out of the repository.
- Save .gitignore.
- Confirm that .gitignore appears in the VS Code Explorer sidebar.
File missing from the Explorer?
Confirm that the filename starts with a period. Check that VS Code saved it inside the open project folder.
Still stuck? Help me check why my .gitignore file is missing from the VS Code project folder.
- Create versions.tf beside .gitignore in the VS Code Explorer.
- Pin Terraform and the required providers by pasting this configuration into versions.tf:
terraform {
required_version = "= 1.16.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "= 5.8.0"
}
archive = {
source = "hashicorp/archive"
version = "= 2.8.1"
}
random = {
source = "hashicorp/random"
version = "= 3.9.1"
}
}
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
resource_provider_registrations = "none"
resource_providers_to_register = [
"Microsoft.EventGrid",
"Microsoft.KeyVault",
"Microsoft.ServiceBus",
"Microsoft.Storage",
"Microsoft.Web"
]
}
What does this configuration do?
- The Terraform constraint requires version 1.16.5.
- The provider constraints pin AzureRM to 5.8.0, Archive to 2.8.1, and Random to 3.9.1.
- The AzureRM provider targets the subscription supplied through var.subscription_id.
- The registration list limits setup to the Azure resource providers required by this project.
- Save versions.tf.
- Confirm that the file contains one Terraform block plus one AzureRM provider block.
Seeing red underlines in versions.tf?
Check each closing brace against the reference code. Confirm that every version value remains inside double quotes.
Need another pair of eyes? Help me find the syntax problem in my versions.tf file.
- Create variables.tf beside versions.tf in the VS Code Explorer.
- Define the deployment inputs by pasting this code into variables.tf:
variable "subscription_id" {
description = "Azure subscription ID used for the portfolio deployment."
type = string
}
variable "location" {
description = "Azure region for all project resources."
type = string
default = "eastus"
}
How do these variables help?
- The subscription_id variable keeps the deployment target outside the reusable provider configuration.
- The location variable places the project in eastus by default.
- Save variables.tf.
- Confirm that both variable blocks appear in the editor.
Variables not recognized?
Check that both blocks begin with the singular keyword variable. Confirm that the variable names match the provider references exactly.
Still seeing a problem? Help me troubleshoot my Terraform variable declarations.
- Create terraform.tfvars.example beside variables.tf in the VS Code Explorer.
- Add the safe example values by pasting this code into terraform.tfvars.example:
subscription_id = "00000000-0000-0000-0000-000000000000"
location = "eastus"
Why keep an example file?
The example documents the required inputs without containing your real subscription ID. Your personal terraform.tfvars remains local because .gitignore excludes it.
- Save terraform.tfvars.example.
- Confirm that the example contains the placeholder subscription ID plus the eastus location.
Example file has the wrong extension?
Confirm that the complete filename is terraform.tfvars.example. Remove any extra text extension added by the operating system.
Need help checking the filename? Help me verify my Terraform variable example file.
Your personal variable file must point Terraform at the subscription you already confirmed. The subscription ID identifies the deployment target without exposing a credential.
- Return to the Windows PowerShell terminal from the previous step.
- Display the selected subscription again by running:
az account show --output table
What does this check confirm?
The command displays the active Azure subscription for the signed-in CLI session. Use the subscription you intend to charge against the promotional credit.
- Record the displayed subscription ID here: your selected subscription ID.
- Duplicate terraform.tfvars.example as terraform.tfvars in the VS Code Explorer.
- Replace 00000000-0000-0000-0000-000000000000 with your selected subscription ID.
- Keep eastus as the location.
- Save terraform.tfvars.
Keep tfvars local
Commit terraform.tfvars.example as documentation. Keep terraform.tfvars on your workstation because it contains subscription-specific configuration.
- Initialize the open Terraform project folder by running:
terraform init
What does initialization do?
Terraform downloads the pinned providers needed by this configuration. It also prepares the local folder for validation and planning.
You should see Terraform report that initialization completed successfully. The VS Code Explorer also shows a new local Terraform data folder.
Initialization failed?
- Confirm that Windows PowerShell is running inside the open VS Code project folder.
- Check that Terraform reports version 1.16.5.
- Check your network connection if a provider download fails.
Still blocked? Help me diagnose why terraform init failed with these pinned providers.
Build the secure Azure foundation
Terraform resolves references between resources to determine deployment order. A generated suffix keeps globally scoped names unique.
The routing path uses a managed identity with Azure RBAC instead of a Service Bus connection string. A separate Azure Key Vault provides an RBAC-enabled location for secrets that may be needed by later workloads.
Why Event Grid with Service Bus?
Event Grid accepts discrete order events and routes them to interested destinations. Service Bus provides the transactional queue that holds a processing copy until a consumer handles it.
This pairing separates event routing from reliable work processing. Each service has one clear job.
- Create main.tf beside versions.tf in the VS Code Explorer.
- Add the current caller lookup plus unique naming foundation by pasting this code into main.tf:
data "azurerm_client_config" "current" {}
resource "random_string" "suffix" {
length = 6
upper = false
special = false
}
locals {
prefix = "azmsg-${random_string.suffix.result}"
tags = {
project = "azure-messaging-portfolio"
environment = "demo"
managed_by = "terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "rg-${local.prefix}"
location = var.location
tags = local.tags
}
How does Terraform name the deployment?
- The client configuration exposes the tenant plus the signed-in caller for later RBAC assignments.
- The random string supplies a six-character lowercase suffix.
- The local prefix applies the same suffix across related resource names.
- The resource group becomes the lifecycle boundary for the Azure foundation.
- Save main.tf.
- Check the current Terraform configuration by running:
terraform validate
What does validation check?
Terraform checks the initialized configuration for syntax problems and inconsistent references. You should see confirmation that the configuration is valid.
Naming configuration is invalid?
Check the interpolation inside local.prefix. Confirm that random_string.suffix.result matches the resource name exactly.
Need help with the reference? Help me troubleshoot the naming section in main.tf.
- Append the hosting resources below the resource group block in main.tf by pasting:
resource "azurerm_storage_account" "functions" {
name = "stazmsg${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
min_tls_version = "TLS1_2"
tags = local.tags
}
resource "azurerm_service_plan" "functions" {
name = "asp-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux"
sku_name = "B1"
tags = local.tags
}
What do these hosting resources provide?
- The storage account supplies the host storage required by the Function App.
- The service plan supplies dedicated Linux compute on the B1 tier.
- Both resources inherit the generated location plus the shared project tags.
- Save main.tf.
- Check the hosting configuration by running:
terraform validate
What does this validation prove?
Terraform can resolve the storage account plus the Linux service plan against the resource group. A valid result confirms that their references are internally consistent.
Hosting resources do not validate?
Check the storage account name for hyphens because Azure storage account names use lowercase letters plus numbers. Confirm that both resources reference azurerm_resource_group.main.
Still blocked? Help me troubleshoot the storage account or B1 Linux service plan configuration.
- Append the Service Bus namespace plus queue below the service plan block in main.tf by pasting:
resource "azurerm_servicebus_namespace" "messaging" {
name = "sb-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
sku = "Standard"
minimum_tls_version = "1.2"
local_auth_enabled = false
tags = local.tags
}
resource "azurerm_servicebus_queue" "orders" {
name = "orders"
namespace_id = azurerm_servicebus_namespace.messaging.id
dead_lettering_on_message_expiration = true
}
How is the queue secured?
- The Standard namespace supports the queue foundation used now plus topic fan-out later.
- Disabling local authentication removes connection-string access to Service Bus.
- The orders queue becomes the single processing destination for order events.
- Expired messages can move to the dead-letter path for investigation.
- Save main.tf.
- Check the messaging configuration by running:
terraform validate
What does this validation prove?
Terraform can connect the orders queue to the Service Bus namespace through its managed resource ID. The valid result confirms the dependency.
Queue reference is invalid?
Confirm that namespace_id points to azurerm_servicebus_namespace.messaging.id. Check that the namespace resource appears above the queue block.
Need help with the dependency? Help me fix the Service Bus namespace or orders queue reference.
- Append the Key Vault plus Event Grid topic below the queue block in main.tf by pasting:
resource "azurerm_key_vault" "main" {
name = "kv-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
rbac_authorization_enabled = true
soft_delete_retention_days = 7
purge_protection_enabled = false
tags = local.tags
}
resource "azurerm_eventgrid_topic" "orders" {
name = "eg-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
input_schema = "EventGridSchema"
local_auth_enabled = true
identity {
type = "SystemAssigned"
}
tags = local.tags
}
What security foundation does this add?
- Key Vault uses Azure RBAC for authorization.
- The seven-day soft-delete period protects the vault from immediate permanent removal.
- The Event Grid custom topic accepts order events using the Event Grid schema.
- The topic receives a system-assigned identity for passwordless delivery to Service Bus.
- Save main.tf.
- Check the vault plus event topic configuration by running:
terraform validate
What does this validation prove?
Terraform can resolve the signed-in tenant for Key Vault. It can also attach a system-assigned identity to the Event Grid topic.
Vault or topic configuration is invalid?
Confirm that the vault tenant uses data.azurerm_client_config.current.tenant_id. Check that the Event Grid identity block sits inside the topic resource.
Still seeing an error? Help me troubleshoot the Key Vault or Event Grid topic configuration.
- Append the Event Grid sender role plus queue route below the Event Grid topic in main.tf by pasting:
resource "azurerm_role_assignment" "eventgrid_sender" {
scope = azurerm_servicebus_namespace.messaging.id
role_definition_name = "Azure Service Bus Data Sender"
principal_id = azurerm_eventgrid_topic.orders.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_eventgrid_event_subscription" "queue_route" {
name = "orders-to-queue"
scope = azurerm_eventgrid_topic.orders.id
service_bus_queue_id = azurerm_servicebus_queue.orders.id
delivery_identity {
type = "SystemAssigned"
}
depends_on = [azurerm_role_assignment.eventgrid_sender]
}
How does the queue route authenticate?
- The sender role lets the Event Grid topic identity send messages into the Service Bus namespace.
- The orders-to-queue event subscription routes matching events into the orders queue.
- The explicit dependency waits for sender authorization before creating the managed-identity delivery route.
- Save main.tf.
- Check the managed-identity route by running:
terraform validate
What does this validation prove?
Terraform can resolve the Event Grid principal plus its Service Bus destination. The dependency graph also contains the sender role required before route creation.
Identity reference is invalid?
Check the index in identity[0].principal_id. Confirm that the event subscription depends on azurerm_role_assignment.eventgrid_sender.
Need help with the route? Help me troubleshoot the Event Grid sender role or queue event subscription.
- Append the empty Function App shell below the queue route in main.tf by pasting:
resource "azurerm_linux_function_app" "processor" {
name = "func-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
service_plan_id = azurerm_service_plan.functions.id
storage_account_name = azurerm_storage_account.functions.name
storage_account_access_key = azurerm_storage_account.functions.primary_access_key
https_only = true
identity {
type = "SystemAssigned"
}
site_config {
always_on = true
minimum_tls_version = "1.2"
}
tags = local.tags
}
What does the Function App shell include?
- The Function App runs on the B1 Linux service plan.
- The host uses the dedicated storage account required by Azure Functions.
- HTTPS plus TLS settings protect inbound traffic.
- The system-assigned identity gives the empty shell an Azure principal for later authorization.
- Save main.tf.
- Check the Function App shell by running:
terraform validate
What does this validation prove?
Terraform can connect the Function App to its service plan plus host storage. The valid result confirms that the shell has all required infrastructure references.
Function App shell is invalid?
Confirm that service_plan_id references the Linux service plan. Check both storage account references against azurerm_storage_account.functions.
Still blocked? Help me troubleshoot the empty Linux Function App shell.
- Append the remaining RBAC assignments below the Function App block in main.tf by pasting:
resource "azurerm_role_assignment" "function_queue_receiver" {
scope = azurerm_servicebus_queue.orders.id
role_definition_name = "Azure Service Bus Data Receiver"
principal_id = azurerm_linux_function_app.processor.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_role_assignment" "function_key_vault_reader" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_function_app.processor.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_role_assignment" "learner_servicebus_owner" {
scope = azurerm_servicebus_namespace.messaging.id
role_definition_name = "Azure Service Bus Data Owner"
principal_id = data.azurerm_client_config.current.object_id
}
Who receives access?
- The Function identity can receive messages from the orders queue.
- The Function identity can read secret contents from the RBAC-enabled vault.
- Your signed-in Azure identity receives Service Bus data access for portal validation.
- Each assignment uses the narrowest resource scope needed for its purpose.
- Save main.tf.
- Check the complete foundation configuration by running:
terraform validate
What does the final validation prove?
Terraform can resolve each role assignment to its intended identity plus resource scope. The full foundation is now internally consistent.
Role assignments do not validate?
Check each principal_id against the intended identity. Confirm that the receiver role uses the queue scope while the Key Vault role uses the vault scope.
Need help mapping the roles? Help me troubleshoot the Function and learner RBAC assignments.
Review and deploy the foundation
Outputs expose generated names without making you search through Terraform state. They also provide the exact resource names needed for later tests.
- Create outputs.tf beside main.tf in the VS Code Explorer.
- Expose the generated resource names by pasting this code into outputs.tf:
output "resource_group_name" {
description = "Resource group containing the project."
value = azurerm_resource_group.main.name
}
output "event_grid_topic_name" {
description = "Event Grid custom topic used by send-event.ps1."
value = azurerm_eventgrid_topic.orders.name
}
output "service_bus_namespace_name" {
description = "Service Bus namespace containing the queue and topic."
value = azurerm_servicebus_namespace.messaging.name
}
output "function_app_name" {
description = "Function App hosting OrderProcessor."
value = azurerm_linux_function_app.processor.name
}
output "key_vault_uri" {
description = "Key Vault URI supplied to the Function App."
value = azurerm_key_vault.main.vault_uri
}
Why define outputs now?
These outputs surface the generated resource group, Event Grid topic, Service Bus namespace, Function App, and Key Vault URI. Later commands can use the generated names without duplicating naming logic.
- Save outputs.tf.
- Check the output references by running:
terraform validate
What does this validation prove?
Terraform can resolve every output to a resource declared in main.tf. The configuration is ready for comparison before deployment.
An output reference is invalid?
Compare each output value with the matching resource label in main.tf. Pay close attention to underscores in the Service Bus namespace output.
Still seeing an error? Help me troubleshoot the resource references in outputs.tf.
Before creating billable resources, compare your cumulative files with the reference. Your personal terraform.tfvars should differ only through its selected subscription ID.
✔️ Awesome, I've got everything!
Great. Save every Terraform file before running the deployment checks.
ⓧ I'd like to double check the full code
Compare each file below with the matching file in your open VS Code project folder.
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
terraform.tfvars
function.zip
crash.log
terraform {
required_version = "= 1.16.5"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "= 5.8.0"
}
archive = {
source = "hashicorp/archive"
version = "= 2.8.1"
}
random = {
source = "hashicorp/random"
version = "= 3.9.1"
}
}
}
provider "azurerm" {
features {}
subscription_id = var.subscription_id
resource_provider_registrations = "none"
resource_providers_to_register = [
"Microsoft.EventGrid",
"Microsoft.KeyVault",
"Microsoft.ServiceBus",
"Microsoft.Storage",
"Microsoft.Web"
]
}
variable "subscription_id" {
description = "Azure subscription ID used for the portfolio deployment."
type = string
}
variable "location" {
description = "Azure region for all project resources."
type = string
default = "eastus"
}
subscription_id = "00000000-0000-0000-0000-000000000000"
location = "eastus"
Your untracked terraform.tfvars uses your selected subscription ID with the same eastus location.
data "azurerm_client_config" "current" {}
resource "random_string" "suffix" {
length = 6
upper = false
special = false
}
locals {
prefix = "azmsg-${random_string.suffix.result}"
tags = {
project = "azure-messaging-portfolio"
environment = "demo"
managed_by = "terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "rg-${local.prefix}"
location = var.location
tags = local.tags
}
resource "azurerm_storage_account" "functions" {
name = "stazmsg${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
min_tls_version = "TLS1_2"
tags = local.tags
}
resource "azurerm_service_plan" "functions" {
name = "asp-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux"
sku_name = "B1"
tags = local.tags
}
resource "azurerm_servicebus_namespace" "messaging" {
name = "sb-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
sku = "Standard"
minimum_tls_version = "1.2"
local_auth_enabled = false
tags = local.tags
}
resource "azurerm_servicebus_queue" "orders" {
name = "orders"
namespace_id = azurerm_servicebus_namespace.messaging.id
dead_lettering_on_message_expiration = true
}
resource "azurerm_key_vault" "main" {
name = "kv-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
rbac_authorization_enabled = true
soft_delete_retention_days = 7
purge_protection_enabled = false
tags = local.tags
}
resource "azurerm_eventgrid_topic" "orders" {
name = "eg-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
input_schema = "EventGridSchema"
local_auth_enabled = true
identity {
type = "SystemAssigned"
}
tags = local.tags
}
resource "azurerm_role_assignment" "eventgrid_sender" {
scope = azurerm_servicebus_namespace.messaging.id
role_definition_name = "Azure Service Bus Data Sender"
principal_id = azurerm_eventgrid_topic.orders.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_eventgrid_event_subscription" "queue_route" {
name = "orders-to-queue"
scope = azurerm_eventgrid_topic.orders.id
service_bus_queue_id = azurerm_servicebus_queue.orders.id
delivery_identity {
type = "SystemAssigned"
}
depends_on = [azurerm_role_assignment.eventgrid_sender]
}
resource "azurerm_linux_function_app" "processor" {
name = "func-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
service_plan_id = azurerm_service_plan.functions.id
storage_account_name = azurerm_storage_account.functions.name
storage_account_access_key = azurerm_storage_account.functions.primary_access_key
https_only = true
identity {
type = "SystemAssigned"
}
site_config {
always_on = true
minimum_tls_version = "1.2"
}
tags = local.tags
}
resource "azurerm_role_assignment" "function_queue_receiver" {
scope = azurerm_servicebus_queue.orders.id
role_definition_name = "Azure Service Bus Data Receiver"
principal_id = azurerm_linux_function_app.processor.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_role_assignment" "function_key_vault_reader" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_function_app.processor.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_role_assignment" "learner_servicebus_owner" {
scope = azurerm_servicebus_namespace.messaging.id
role_definition_name = "Azure Service Bus Data Owner"
principal_id = data.azurerm_client_config.current.object_id
}
output "resource_group_name" {
description = "Resource group containing the project."
value = azurerm_resource_group.main.name
}
output "event_grid_topic_name" {
description = "Event Grid custom topic used by send-event.ps1."
value = azurerm_eventgrid_topic.orders.name
}
output "service_bus_namespace_name" {
description = "Service Bus namespace containing the queue and topic."
value = azurerm_servicebus_namespace.messaging.name
}
output "function_app_name" {
description = "Function App hosting OrderProcessor."
value = azurerm_linux_function_app.processor.name
}
output "key_vault_uri" {
description = "Key Vault URI supplied to the Function App."
value = azurerm_key_vault.main.vault_uri
}
- Format every Terraform file by running:
terraform fmt
What does formatting change?
Terraform applies its standard spacing plus alignment to the configuration files. This keeps later reviews focused on infrastructure changes.
- Validate the formatted configuration by running:
terraform validate
What should validation confirm?
Terraform should report that the complete configuration is valid. That result confirms syntax plus internal references before Azure receives any changes.
Before you generate the plan, do you expect Terraform to create new resources or modify existing infrastructure?
- Preview the Azure changes by running:
terraform plan
What should the plan show?
The plan should propose new foundation resources because this Terraform state has not deployed anything yet. Review the subscription plus the eastus location before continuing.
Plan failed before deployment?
- Confirm that terraform.tfvars contains your selected subscription ID.
- Confirm that your Azure CLI session still targets the intended subscription.
- Retry after correcting any validation error reported above the plan summary.
Need help reading the failure? Help me troubleshoot my Terraform plan before I create Azure resources.
Review the billable resources
This apply creates a B1 App Service plan plus a Service Bus Standard namespace. Both continue to accrue charges until Terraform destroys them.
Your promotional Azure credit covers the project path. The cleanup section removes the environment when you finish.
Before you apply the plan, do you expect Terraform to create the complete foundation in one dependency-aware operation?
- Create the planned Azure resources by running:
terraform apply
What happens during apply?
Terraform presents the plan for confirmation before creating resources. Approve only after checking the subscription plus the proposed additions.
Terraform then creates resources according to their dependencies. The identity-backed routes wait for their required principals plus roles.
Provisioning can take several minutes because Azure creates the service plan, Function App, identities, and role assignments. A quiet period during this stage is normal.
You should see Terraform report a successful apply. That is the hard part done: the secure queue foundation now exists in Azure.
Apply failed or authorization was denied?
- Confirm that your Azure identity can create role assignments in the selected subscription.
- Retry the apply if a newly created identity was not immediately available.
- Review the first reported error before changing the configuration.
Still blocked? Help me diagnose my Terraform apply failure for this Azure foundation.
Before you reveal the outputs, which generated names do you expect to share the same six-character suffix?
- Display the generated resource names by running:
terraform output
What should the outputs reveal?
Terraform prints the generated resource group, Event Grid topic, Service Bus namespace, Function App, and Key Vault URI. The generated names should share the same random suffix.
- Record the resource group name here: your generated resource group name.
- Record the Event Grid topic name here: your generated Event Grid topic name.
- Record the Service Bus namespace name here: your generated Service Bus namespace name.
- Record the Function App name here: your generated Function App name.
- Record the Key Vault URI here: your generated Key Vault URI.
- Go to the Azure portal in your browser.
- Search for your generated resource group name.
- Open the generated resource group.
- Confirm that its resource list includes the storage account, service plan, Function App, Key Vault, Service Bus namespace, and Event Grid topic.
- Open your generated Function App name.
- Confirm that the Function App shell exists with no deployed order processor yet.
- Return to the generated resource group.
- Open your generated Service Bus namespace name.
- Inspect the namespace queue list to confirm that orders exists.
Your portal view now matches Terraform state. The empty Function App shell plus the orders queue prove that the secure foundation is live.
Your Azure foundation is deployed through reproducible Terraform. Next, you will package the PowerShell order processor and prove that the queue can deliver work to it.
Deploy the Order Processor
Your Terraform foundation now routes Event Grid events into the Service Bus orders queue. The deployed resources already use managed identities with scoped Azure RBAC roles.
The Azure Functions app still has no processor code. The queue also holds one processing copy of each event. This step deploys the processor and lets you see that single-copy behavior firsthand.
In this step, get ready to:
- Build a PowerShell queue-triggered Function package.
- Deploy the package with identity-based Service Bus settings.
- Publish an order event and observe the queue's single processing copy.
Create the Function package
A PowerShell Function keeps its host configuration in host.json. Each function then gets its own folder containing function.json and run.ps1.
- In the Explorer sidebar in Visual Studio Code, create a function folder inside the project folder from earlier.
- Create host.json inside the function folder.
- Define the Function host by replacing the contents of host.json with this configuration:
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.0.0, 5.0.0)"
},
"logging": {
"logLevel": {
"Function.OrderProcessor": "Information",
"default": "Warning"
}
}
}
What Does This Configuration Do?
- The host version selects the Azure Functions v4 configuration model.
- The extension bundle supplies the Service Bus trigger binding used by the PowerShell Function.
- The logging settings keep OrderProcessor messages visible while reducing unrelated host logs.
- Save function/host.json.
- Confirm that host.json appears inside the function folder in the Explorer sidebar.
Host Configuration Showing an Error?
Check that every property name uses double quotes. Confirm that the final property in each JSON object has no trailing comma.
Still stuck? Help me check my Azure Functions host.json structure.
- Create an OrderProcessor folder inside the function folder.
- Create function.json inside function/OrderProcessor.
- Define the queue trigger by replacing the contents of function.json with this configuration:
{
"bindings": [
{
"name": "mySbMsg",
"type": "serviceBusTrigger",
"direction": "in",
"queueName": "orders",
"connection": "ServiceBusConnection"
}
]
}
How Does the Trigger Work?
- The serviceBusTrigger binding starts the Function when a message reaches the orders queue.
- The binding passes the message body to the mySbMsg parameter.
- The ServiceBusConnection prefix links the binding to the identity-based application settings you add next.
- Save function/OrderProcessor/function.json.
- Confirm that function.json appears inside the OrderProcessor folder.
Queue Trigger Not Recognized?
Check that the folder is named OrderProcessor. Confirm that queueName is set to orders.
Need another pair of eyes? Help me troubleshoot my Service Bus trigger configuration.
The binding delivers the queue message to PowerShell. The script logs the event body and confirms that the Key Vault URI reached the Function environment.
- Create run.ps1 inside function/OrderProcessor.
- Implement the processor by replacing the contents of run.ps1 with this script:
param([string] $mySbMsg)
Write-Host "Processed order event: $mySbMsg"
Write-Host "Key Vault URI configured: $env:KEY_VAULT_URI"
What Does the Processor Log?
- The mySbMsg parameter receives the message body from the binding.
- The first log entry proves that OrderProcessor handled the order event.
- The second log entry proves that the Function received the configured Key Vault URI.
- Save function/OrderProcessor/run.ps1.
- Confirm that the OrderProcessor folder now contains function.json and run.ps1.
Processor File in the Wrong Place?
Make sure run.ps1 sits beside function.json inside function/OrderProcessor. The Function name comes from that folder name.
Need help checking the layout? Help me verify my PowerShell Function folder structure.
✔️ Awesome, I've got everything!
Your Function package now contains the host configuration, queue binding, and PowerShell processor.
ⓧ I'd like to double check the full code
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.0.0, 5.0.0)"
},
"logging": {
"logLevel": {
"Function.OrderProcessor": "Information",
"default": "Warning"
}
}
}
Host File Check
Compare this reference with function/host.json. It configures the extension bundle and logging for the package.
{
"bindings": [
{
"name": "mySbMsg",
"type": "serviceBusTrigger",
"direction": "in",
"queueName": "orders",
"connection": "ServiceBusConnection"
}
]
}
Binding File Check
Compare this reference with function/OrderProcessor/function.json. It connects OrderProcessor to the orders queue.
param([string] $mySbMsg)
Write-Host "Processed order event: $mySbMsg"
Write-Host "Key Vault URI configured: $env:KEY_VAULT_URI"
Processor File Check
Compare this reference with function/OrderProcessor/run.ps1. It logs the message body and the configured Key Vault URI.
Connect the package to the Function App
The Archive provider can package the entire function directory as function.zip during Terraform planning. Your existing .gitignore already keeps that generated package out of Git.
- In main.tf, locate resource "azurerm_linux_function_app" "processor" {.
- Insert the archive data source directly above that resource by pasting this block:
data "archive_file" "function" {
type = "zip"
source_dir = "${path.module}/function"
output_path = "${path.module}/function.zip"
}
What Does the Archive Data Source Do?
- The data source reads the completed function directory.
- The Archive provider writes the package to function.zip.
- Terraform can pass that generated path directly to the Function App resource.
- Save main.tf.
Before you check, consider whether Terraform can resolve the new package from the Function files you just created.
- Check the archive configuration by running this command in the PowerShell terminal from earlier:
terraform validate
What Does This Check Prove?
A successful validation confirms that Terraform can read the archive data source and its file paths. It also checks the configuration's internal consistency.
Archive Path Not Validating?
Confirm that function/host.json exists inside the same project folder as main.tf. Check that the data source uses ${path.module}/function exactly.
Still blocked? Help me troubleshoot my Terraform archive_file data source.
The Function App needs the ZIP path plus runtime settings for Functions v4. Its Service Bus connection uses application settings that point to the namespace and select the system-assigned identity.
- Inside azurerm_linux_function_app.processor, set the deployment fields to match this block:
functions_extension_version = "~4"
https_only = true
zip_deploy_file = data.archive_file.function.output_path
How Is the Package Deployed?
- The extension version keeps the app on the Functions v4 runtime.
- The HTTPS setting requires encrypted web traffic.
- The ZIP deployment field sends the generated package to the existing Function App.
- Inside the same Function App resource, locate the existing identity block.
- Add these application settings directly below the identity block:
app_settings = {
"KEY_VAULT_URI" = azurerm_key_vault.main.vault_uri
"ServiceBusConnection__credential" = "managedidentity"
"ServiceBusConnection__fullyQualifiedNamespace" = "${azurerm_servicebus_namespace.messaging.name}.servicebus.windows.net"
"WEBSITE_RUN_FROM_PACKAGE" = "1"
}
How Does Identity-Based Access Work?
- The ServiceBusConnection__credential setting selects managed identity authentication.
- The ServiceBusConnection__fullyQualifiedNamespace setting points the binding at the deployed Service Bus namespace.
- The existing receiver role allows the Function identity to consume messages from the orders queue.
- The package setting tells Azure Functions to run the deployed ZIP package.
- Save main.tf.
- Check the deployment settings by running this command:
terraform validate
What Should This Validation Confirm?
Terraform should accept the ZIP deployment path and all four application settings. This proves that the new values reference resources already managed in main.tf.
PowerShell 7.6 runs on the existing B1 Linux service plan. The Function App also stays active between queue messages.
- Set the Function App's site_config block to match this configuration:
site_config {
always_on = true
minimum_tls_version = "1.2"
application_stack {
powershell_core_version = "7.6"
}
}
What Does the Runtime Configuration Do?
- The always-on setting keeps the dedicated Function host available for queue messages.
- The TLS setting controls the minimum protocol version accepted by the app.
- The application stack selects PowerShell 7.6 for the processor.
- Save main.tf.
Before you validate again, predict whether the complete Function App resource now has everything required to deploy the package.
- Validate the completed Function App configuration by running:
terraform validate
What Does the Final Validation Show?
A successful result confirms that the archive, application settings, runtime stack, and existing identity references form a valid Terraform configuration.
Function App Configuration Not Validating?
Check that app_settings and site_config remain inside azurerm_linux_function_app.processor. Confirm that each closing brace matches its opening block.
Need help tracing the block structure? Help me debug my Terraform Function App configuration.
✔️ Awesome, I've got everything!
Your Terraform configuration can now package the PowerShell files and deploy them to the existing Function App.
ⓧ I'd like to double check the full code
data "azurerm_client_config" "current" {}
resource "random_string" "suffix" {
length = 6
upper = false
special = false
}
locals {
prefix = "azmsg-${random_string.suffix.result}"
tags = {
project = "azure-messaging-portfolio"
environment = "demo"
managed_by = "terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "rg-${local.prefix}"
location = var.location
tags = local.tags
}
resource "azurerm_storage_account" "functions" {
name = "stazmsg${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
min_tls_version = "TLS1_2"
tags = local.tags
}
resource "azurerm_service_plan" "functions" {
name = "asp-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux"
sku_name = "B1"
tags = local.tags
}
resource "azurerm_servicebus_namespace" "messaging" {
name = "sb-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
sku = "Standard"
minimum_tls_version = "1.2"
local_auth_enabled = false
tags = local.tags
}
resource "azurerm_servicebus_queue" "orders" {
name = "orders"
namespace_id = azurerm_servicebus_namespace.messaging.id
dead_lettering_on_message_expiration = true
}
resource "azurerm_key_vault" "main" {
name = "kv-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
rbac_authorization_enabled = true
soft_delete_retention_days = 7
purge_protection_enabled = false
tags = local.tags
}
resource "azurerm_eventgrid_topic" "orders" {
name = "eg-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
input_schema = "EventGridSchema"
local_auth_enabled = true
identity {
type = "SystemAssigned"
}
tags = local.tags
}
resource "azurerm_role_assignment" "eventgrid_sender" {
scope = azurerm_servicebus_namespace.messaging.id
role_definition_name = "Azure Service Bus Data Sender"
principal_id = azurerm_eventgrid_topic.orders.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_eventgrid_event_subscription" "queue_route" {
name = "orders-to-queue"
scope = azurerm_eventgrid_topic.orders.id
service_bus_queue_id = azurerm_servicebus_queue.orders.id
delivery_identity {
type = "SystemAssigned"
}
depends_on = [azurerm_role_assignment.eventgrid_sender]
}
data "archive_file" "function" {
type = "zip"
source_dir = "${path.module}/function"
output_path = "${path.module}/function.zip"
}
resource "azurerm_linux_function_app" "processor" {
name = "func-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
service_plan_id = azurerm_service_plan.functions.id
storage_account_name = azurerm_storage_account.functions.name
storage_account_access_key = azurerm_storage_account.functions.primary_access_key
functions_extension_version = "~4"
https_only = true
zip_deploy_file = data.archive_file.function.output_path
identity {
type = "SystemAssigned"
}
app_settings = {
"KEY_VAULT_URI" = azurerm_key_vault.main.vault_uri
"ServiceBusConnection__credential" = "managedidentity"
"ServiceBusConnection__fullyQualifiedNamespace" = "${azurerm_servicebus_namespace.messaging.name}.servicebus.windows.net"
"WEBSITE_RUN_FROM_PACKAGE" = "1"
}
site_config {
always_on = true
minimum_tls_version = "1.2"
application_stack {
powershell_core_version = "7.6"
}
}
tags = local.tags
}
resource "azurerm_role_assignment" "function_queue_receiver" {
scope = azurerm_servicebus_queue.orders.id
role_definition_name = "Azure Service Bus Data Receiver"
principal_id = azurerm_linux_function_app.processor.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_role_assignment" "function_key_vault_reader" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_function_app.processor.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_role_assignment" "learner_servicebus_owner" {
scope = azurerm_servicebus_namespace.messaging.id
role_definition_name = "Azure Service Bus Data Owner"
principal_id = data.azurerm_client_config.current.object_id
}
Complete Configuration Check
Use this cumulative main.tf as a comparison reference. It includes the secure queue foundation plus the Function package and runtime configuration from this step.
Deploy and test the processor
The publisher script retrieves the Event Grid endpoint and a temporary access key. The access key is sensitive, but this script holds it only in memory during the request.
- Create send-event.ps1 inside the same project folder as main.tf.
- Add the script parameters and Event Grid lookups by pasting this first section:
param(
[Parameter(Mandatory = $true)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $true)]
[string] $TopicName,
[string] $Subject = "/orders/standard/1001"
)
$endpoint = az eventgrid topic show `
--name $TopicName `
--resource-group $ResourceGroupName `
--query "endpoint" `
--output tsv
$key = az eventgrid topic key list `
--name $TopicName `
--resource-group $ResourceGroupName `
--query "key1" `
--output tsv
What Does the First Section Do?
- The mandatory parameters identify the deployed resource group and Event Grid topic.
- The default subject identifies order 1001 as a standard order.
- The Azure CLI lookups retrieve the topic endpoint and temporary publishing key.
- Save send-event.ps1.
- Confirm that send-event.ps1 appears beside main.tf in the Explorer sidebar.
Publisher Parameters Showing a Syntax Problem?
Check the commas between the three parameters. Confirm that each Azure CLI continuation line ends with the PowerShell backtick shown in the reference.
Need help checking the first section? Help me debug the parameter and Azure CLI lookup section of send-event.ps1.
The second half builds one Event Grid schema event. It posts the event and prints the HTTP response status.
- Append the event body and HTTP request below the existing $key lookup:
$eventBody = @{
id = [guid]::NewGuid().ToString()
eventType = "Order.Created"
subject = $Subject
eventTime = Get-Date -Format s
data = @{
orderId = $Subject.Split("/")[-1]
source = "portfolio-demo"
}
dataVersion = "1.0"
}
$body = "[" + (ConvertTo-Json $eventBody) + "]"
$response = Invoke-WebRequest `
-Uri $endpoint `
-Method POST `
-Body $body `
-Headers @{ "aeg-sas-key" = $key }
Write-Host "Event Grid response: $($response.StatusCode)"
How Is the Order Event Published?
- The event body records a unique ID and the Order.Created event type.
- The subject carries the order category and order ID.
- The web request posts the JSON body to the Event Grid endpoint.
- The final log line exposes the HTTP status without printing the access key.
- Save the completed send-event.ps1 file.
Publisher Script Showing an Editor Error?
Confirm that the event body closes both hashtables. Check that the request headers use the existing $key variable.
Still seeing a problem? Help me debug the event body and HTTP request in send-event.ps1.
✔️ Awesome, I've got everything!
Your publisher can now discover the deployed Event Grid topic and submit a standard order event.
ⓧ I'd like to double check the full code
param(
[Parameter(Mandatory = $true)]
[string] $ResourceGroupName,
[Parameter(Mandatory = $true)]
[string] $TopicName,
[string] $Subject = "/orders/standard/1001"
)
$endpoint = az eventgrid topic show `
--name $TopicName `
--resource-group $ResourceGroupName `
--query "endpoint" `
--output tsv
$key = az eventgrid topic key list `
--name $TopicName `
--resource-group $ResourceGroupName `
--query "key1" `
--output tsv
$eventBody = @{
id = [guid]::NewGuid().ToString()
eventType = "Order.Created"
subject = $Subject
eventTime = Get-Date -Format s
data = @{
orderId = $Subject.Split("/")[-1]
source = "portfolio-demo"
}
dataVersion = "1.0"
}
$body = "[" + (ConvertTo-Json $eventBody) + "]"
$response = Invoke-WebRequest `
-Uri $endpoint `
-Method POST `
-Body $body `
-Headers @{ "aeg-sas-key" = $key }
Write-Host "Event Grid response: $($response.StatusCode)"
Complete Publisher Check
Compare this reference with send-event.ps1. The complete script retrieves the publishing details, creates the order event, sends it, and prints the response status.
Terraform now has a Function package to deploy. This update can take a few minutes while Azure refreshes the Function App package and runtime.
Before you apply, consider which existing resource Terraform will update to attach the new processor package.
- Deploy the Function package by running this command:
terraform apply
What Does This Apply Change?
Terraform creates function.zip from the local Function directory. It updates the existing Function App with the package, runtime, and application settings.
- Approve the plan using the same response you used for the foundation deployment.
- Wait for Terraform to report a successful apply.
Function Deployment Failed?
Confirm that function.zip was generated beside main.tf. Check the preceding Terraform output for the resource that failed.
Need help reading the failure? Help me troubleshoot my Terraform ZIP deployment to the Linux Function App.
That is the processor deployed. Your existing Function App now has runnable code connected to the orders queue.
- Display the generated Azure resource names by running:
terraform output
Which Outputs Do You Need?
The publisher needs the values shown beside resource_group_name and event_grid_topic_name.
Before you publish, consider whether the queue will keep a copy after OrderProcessor handles the event.
- Replace <resource-group-name> in the command below with the value shown beside resource_group_name.
- Replace <event-grid-topic-name> with the value shown beside event_grid_topic_name.
- Publish the standard order event by running the completed command:
./send-event.ps1 -ResourceGroupName "<resource-group-name>" -TopicName "<event-grid-topic-name>"
What Should You See?
A successful request prints Event Grid response: 200 in the terminal. That status proves Event Grid accepted the order event.
- Open the Azure portal in your browser.
- Search for the Function App name shown by terraform output.
- Select the deployed Function App.
- Confirm that OrderProcessor appears under the Function App.
- Return to the Service Bus namespace name shown by terraform output.
- Select the orders queue.
- Confirm that the active message count returns to 0 after the Function processes the event.
Event Accepted but the Queue Is Not Empty?
A new role assignment can take several minutes to become effective. Retry the publish before changing the Terraform configuration if authorization is still propagating.
Confirm that the Function App contains OrderProcessor. Check that the connection prefix remains ServiceBusConnection in function.json.
Still seeing messages in the queue? Help me troubleshoot why OrderProcessor is not consuming Service Bus messages.
You have proved the queue's one-copy behavior. OrderProcessor consumes the work item, which leaves no independently retained copy for another consumer.
Your order processor is live and consuming queue messages through managed identity. Next, you will add independent event copies for analytics and notifications.
Add Topic Fan-Out
Your Terraform deployment already routes each order through an Azure Service Bus queue. In the last step, your Function consumed that processing copy.
The consumed queue event left no copy for analytics or notifications. An Azure Service Bus topic gives each subscription an independent copy. Azure Event Grid can deliver the same order event to this new fan-out path through its existing managed identity.
In this step, get ready to:
- Create the order-events topic with separate analytics and notifications subscriptions.
- Route every published order event to the new topic.
- Prove that each subscription retains an independent event copy.
Create the topic
A topic accepts one message before distributing a copy to every attached subscription. The existing Standard namespace supports this fan-out model.
- Switch back to main.tf in Visual Studio Code.
- Find the azurerm_servicebus_queue.orders resource.
- Add the topic directly below the queue resource by copying this block:
resource "azurerm_servicebus_topic" "order_events" {
name = "order-events"
namespace_id = azurerm_servicebus_namespace.messaging.id
}
What does this resource do?
- The azurerm_servicebus_topic.order_events resource creates the fan-out destination.
- The name value gives the topic its visible Azure name.
- The namespace_id value places the topic inside your existing Service Bus namespace.
- Save main.tf.
- Format the file by running these commands in the PowerShell terminal from earlier:
terraform fmt
terraform validate
What do these commands check?
- The first command applies Terraform's standard formatting to your configuration.
- The second command checks the initialized configuration for syntax problems or invalid references.
Terraform should confirm that the configuration is valid. This proves the new topic resource connects to an existing namespace reference.
Is the topic configuration invalid?
Check that the resource label is order_events. Confirm that namespace_id references azurerm_servicebus_namespace.messaging.id.
Still stuck? Help me troubleshoot my Service Bus topic resource.
Add independent subscriptions
A subscription stores its own copy of each topic message. Analytics can inspect one copy without removing the copy reserved for notifications.
- Stay in main.tf.
- Add the analytics subscription directly below the order_events topic resource by copying this block:
resource "azurerm_servicebus_subscription" "analytics" {
name = "analytics"
topic_id = azurerm_servicebus_topic.order_events.id
max_delivery_count = 10
}
How does the analytics copy work?
- The topic_id value attaches the subscription to order-events.
- The analytics subscription receives its own copy of every message sent to the topic.
- The max_delivery_count value limits repeated delivery attempts to 10.
- Save main.tf.
- Check the analytics subscription reference by running this command:
terraform validate
What does this validation prove?
Terraform resolves the analytics subscription's topic_id reference. A successful validation confirms that the subscription points to the topic resource in the same configuration.
You should see Terraform confirm that the configuration is valid. The analytics subscription is now represented in the deployment plan.
Does the analytics reference fail?
Confirm that topic_id points to azurerm_servicebus_topic.order_events.id. Check every underscore in the resource address.
Need another pair of eyes? Help me find the broken analytics subscription reference.
- Add the notifications subscription directly below the analytics subscription by copying this block:
resource "azurerm_servicebus_subscription" "notifications" {
name = "notifications"
topic_id = azurerm_servicebus_topic.order_events.id
max_delivery_count = 10
}
Why create a second subscription?
The notifications subscription uses the same topic. It maintains a separate message copy with its own delivery lifecycle.
This separation lets analytics and notifications process the event independently. One consumer cannot drain the other consumer's copy.
- Save main.tf.
- Check both subscription resources by running this command:
terraform validate
What is Terraform checking now?
Terraform checks both subscription blocks against the topic resource. It also checks each required argument before Azure receives the configuration.
You should see Terraform confirm that the configuration is valid again. Your configuration now describes one topic with two independent subscriptions.
Does the second subscription fail validation?
Check that the resource label is notifications. Confirm that both subscription resources have unique labels.
Need help comparing the blocks? Help me troubleshoot my two Service Bus subscriptions.
Route events and verify fan-out
The topic can store messages only after an event route targets it. The existing Event Grid identity already has the Azure Service Bus Data Sender role at the namespace scope through Azure RBAC.
- Find the azurerm_eventgrid_event_subscription.queue_route resource in main.tf.
- Add the topic route directly below the queue route by copying this block:
resource "azurerm_eventgrid_event_subscription" "topic_route" {
name = "orders-to-topic"
scope = azurerm_eventgrid_topic.orders.id
service_bus_topic_id = azurerm_servicebus_topic.order_events.id
delivery_identity {
type = "SystemAssigned"
}
depends_on = [azurerm_role_assignment.eventgrid_sender]
}
How does the second route work?
- The scope value listens to the same custom Event Grid topic as the queue route.
- The service_bus_topic_id value sends a second event copy to order-events.
- The delivery_identity block uses the Event Grid topic's system-assigned identity for delivery.
- The depends_on value waits for the sender role assignment before creating the route.
- Save main.tf.
- Format the completed route configuration by running these commands:
terraform fmt
terraform validate
What does this check before deployment?
Terraform formats the new route before checking every resource reference. This catches structural problems before an apply can change Azure.
You should see Terraform confirm that the configuration is valid. The configuration now includes the queue route and the topic route.
Is the topic route invalid?
Confirm that the destination argument is service_bus_topic_id. Check that it references azurerm_servicebus_topic.order_events.id.
Still seeing a validation problem? Help me troubleshoot the Event Grid topic route.
Use these tabs to compare your completed main.tf before changing the deployed environment.
✔️ Awesome, I've got everything!
Great. Double-check that main.tf is saved before you apply the new messaging resources.
ⓧ I'd like to double check the full code
data "azurerm_client_config" "current" {}
resource "random_string" "suffix" {
length = 6
upper = false
special = false
}
locals {
prefix = "azmsg-${random_string.suffix.result}"
tags = {
project = "azure-messaging-portfolio"
environment = "demo"
managed_by = "terraform"
}
}
resource "azurerm_resource_group" "main" {
name = "rg-${local.prefix}"
location = var.location
tags = local.tags
}
resource "azurerm_storage_account" "functions" {
name = "stazmsg${random_string.suffix.result}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "LRS"
min_tls_version = "TLS1_2"
tags = local.tags
}
resource "azurerm_service_plan" "functions" {
name = "asp-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux"
sku_name = "B1"
tags = local.tags
}
resource "azurerm_servicebus_namespace" "messaging" {
name = "sb-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
sku = "Standard"
minimum_tls_version = "1.2"
local_auth_enabled = false
tags = local.tags
}
resource "azurerm_servicebus_queue" "orders" {
name = "orders"
namespace_id = azurerm_servicebus_namespace.messaging.id
dead_lettering_on_message_expiration = true
}
resource "azurerm_servicebus_topic" "order_events" {
name = "order-events"
namespace_id = azurerm_servicebus_namespace.messaging.id
}
resource "azurerm_servicebus_subscription" "analytics" {
name = "analytics"
topic_id = azurerm_servicebus_topic.order_events.id
max_delivery_count = 10
}
resource "azurerm_servicebus_subscription" "notifications" {
name = "notifications"
topic_id = azurerm_servicebus_topic.order_events.id
max_delivery_count = 10
}
resource "azurerm_key_vault" "main" {
name = "kv-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
rbac_authorization_enabled = true
soft_delete_retention_days = 7
purge_protection_enabled = false
tags = local.tags
}
resource "azurerm_eventgrid_topic" "orders" {
name = "eg-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
input_schema = "EventGridSchema"
local_auth_enabled = true
identity {
type = "SystemAssigned"
}
tags = local.tags
}
resource "azurerm_role_assignment" "eventgrid_sender" {
scope = azurerm_servicebus_namespace.messaging.id
role_definition_name = "Azure Service Bus Data Sender"
principal_id = azurerm_eventgrid_topic.orders.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_eventgrid_event_subscription" "queue_route" {
name = "orders-to-queue"
scope = azurerm_eventgrid_topic.orders.id
service_bus_queue_id = azurerm_servicebus_queue.orders.id
delivery_identity {
type = "SystemAssigned"
}
depends_on = [azurerm_role_assignment.eventgrid_sender]
}
resource "azurerm_eventgrid_event_subscription" "topic_route" {
name = "orders-to-topic"
scope = azurerm_eventgrid_topic.orders.id
service_bus_topic_id = azurerm_servicebus_topic.order_events.id
delivery_identity {
type = "SystemAssigned"
}
depends_on = [azurerm_role_assignment.eventgrid_sender]
}
data "archive_file" "function" {
type = "zip"
source_dir = "${path.module}/function"
output_path = "${path.module}/function.zip"
}
resource "azurerm_linux_function_app" "processor" {
name = "func-${local.prefix}"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
service_plan_id = azurerm_service_plan.functions.id
storage_account_name = azurerm_storage_account.functions.name
storage_account_access_key = azurerm_storage_account.functions.primary_access_key
functions_extension_version = "~4"
https_only = true
zip_deploy_file = data.archive_file.function.output_path
identity {
type = "SystemAssigned"
}
app_settings = {
"KEY_VAULT_URI" = azurerm_key_vault.main.vault_uri
"ServiceBusConnection__credential" = "managedidentity"
"ServiceBusConnection__fullyQualifiedNamespace" = "${azurerm_servicebus_namespace.messaging.name}.servicebus.windows.net"
"WEBSITE_RUN_FROM_PACKAGE" = "1"
}
site_config {
always_on = true
minimum_tls_version = "1.2"
application_stack {
powershell_core_version = "7.6"
}
}
tags = local.tags
}
resource "azurerm_role_assignment" "function_queue_receiver" {
scope = azurerm_servicebus_queue.orders.id
role_definition_name = "Azure Service Bus Data Receiver"
principal_id = azurerm_linux_function_app.processor.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_role_assignment" "function_key_vault_reader" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_function_app.processor.identity[0].principal_id
principal_type = "ServicePrincipal"
skip_service_principal_aad_check = true
}
resource "azurerm_role_assignment" "learner_servicebus_owner" {
scope = azurerm_servicebus_namespace.messaging.id
role_definition_name = "Azure Service Bus Data Owner"
principal_id = data.azurerm_client_config.current.object_id
}
How to use this reference
Compare the resource order and identifiers with your saved file. The topic route has no subject filter at this stage, so every order event reaches both subscriptions.
Your existing B1 plan and Service Bus Standard namespace remain billable while deployed. This apply adds the topic resources to that live environment.
- Deploy the fan-out resources by running this command:
- Review the proposed resource additions when Terraform displays the execution plan.
- Enter yes when Terraform asks for approval.
terraform apply
What does this apply change?
Terraform creates the order-events topic with both subscriptions. It also creates the orders-to-topic Event Grid event subscription.
The existing orders-to-queue route remains unchanged. Every event now enters the processing path and the fan-out path.
Terraform should report a successful apply with the new resources added. Your live pipeline now has two delivery destinations.
Did the apply fail?
Read the resource address near the failure. A topic or subscription failure usually points back to the matching block in main.tf.
Need help reading the apply output? Help me diagnose my Terraform apply failure.
- Display the generated Azure resource names by running this command:
terraform output
Which outputs do you need?
The output lists the generated resource group and Event Grid topic names. The publisher script uses these values to target your deployed custom topic.
- Record the resource_group_name value here: your generated resource group name.
- Record the event_grid_topic_name value here: your generated Event Grid topic name.
Before you publish the event, how many independently retained topic copies do you expect to find?
- Publish another standard order event by running this command:
./send-event.ps1 -ResourceGroupName "[[RESOURCE_GROUP_NAME="your generated resource group name"]]" -TopicName "[[EVENT_GRID_TOPIC_NAME="your generated Event Grid topic name"]]"
What does the publisher test prove?
The script sends one Order.Created event to your custom topic. Event Grid evaluates both event subscriptions for that same event.
The queue receives one processing copy. The Service Bus topic sends another copy to each attached subscription.
You should see an HTTP status of 200 in PowerShell. This confirms that Event Grid accepted the event.
Did the publisher test fail?
Confirm that both command values match the latest terraform output results. Retry after a few minutes if Azure is still applying identity permissions.
Still blocked? Help me troubleshoot the Event Grid publishing test.
- Return to the Azure portal from earlier.
- Open the Service Bus namespace named by your service_bus_namespace_name Terraform output.
- Select Topics.
- Select order-events.
- Open Service Bus Explorer.
- Select analytics in the subscription selector.
- Choose Peek Mode.
- Select Peek from start.
You should see the published order event retained in analytics. Peeking leaves the message available for its independent consumer.
- Select notifications in the subscription selector.
- Choose Peek Mode.
- Select Peek from start.
You should see the same order event retained in notifications. You have now proved that both subscriptions received separate copies.
- Return to the Service Bus namespace.
- Select Queues.
- Select the orders queue.
- Check the active message count.
The active message count should return to zero after OrderProcessor consumes the queue copy. The two subscription copies remain available because they have separate delivery state.
Is a subscription missing the event?
Confirm that you selected the subscription before peeking. Use Peek from start so the explorer includes the earliest retained message.
Need help tracing the route? Help me find why one Service Bus subscription has no event.
Your pipeline now handles one processing copy while preserving separate copies for analytics and notifications. Next, you'll document the architecture and turn the working deployment into a portfolio artifact.
Document and Validate the Deployment
Your Azure pipeline now processes each order through the queue. It also retains independent copies for analytics and notifications.
A working deployment is difficult for an interviewer to judge without clear evidence. In this step, you will document the design before using Terraform to prove that the infrastructure still matches the code.
In this step, get ready to:
- Build a README that tells the complete deployment story.
- Confirm that generated files stay outside source control.
- Prove that the deployed infrastructure matches the Terraform configuration.
Build the project story
Your README.md gives the deployment a story that someone can follow. A Mermaid diagram makes both messaging paths visible.
- Create README.md at the top level of the VS Code project folder from Step 1 by pasting this opening content:
# Azure Event-Driven Order Pipeline with Terraform
This project provisions and demonstrates an event-driven order workflow on Microsoft Azure. Terraform owns the infrastructure and deploys a minimal PowerShell Function package.
## Architecture
```mermaid
flowchart LR
P[Order publisher] --> EG[Azure Event Grid custom topic]
EG -->|Managed identity| Q[Service Bus queue: orders]
Q --> F[PowerShell Function: OrderProcessor]
EG -->|Managed identity| T[Service Bus topic: order-events]
T --> A[Subscription: analytics]
T --> N[Subscription: notifications]
F -. Key Vault URI and RBAC .-> KV[Azure Key Vault]
```
What does this section show?
- The publisher sends each event to Azure Event Grid. Event Grid routes the processing copy to Service Bus.
- The topic branch retains one copy per subscription. This makes fan-out visible.
- The dotted line records the Function's authorized access to Azure Key Vault.
- Save README.md.
- Select the Markdown preview control in the editor toolbar.
You should see a left-to-right architecture diagram with separate queue and topic branches.
Diagram missing from the preview?
- Check that the opening and closing Mermaid fences each use three backticks.
- Confirm that flowchart LR sits directly below the opening Mermaid fence.
Still stuck? Help me troubleshoot why the Mermaid flowchart in my README preview is not rendering.
- Add the service responsibilities and security model below the architecture section by pasting:
## Why each service exists
- Event Grid accepts discrete order events and routes them to interested handlers.
- The Service Bus queue provides one reliable processing path for the order processor.
- The Service Bus topic gives independent consumers their own event copies.
- The Function App runs the queue-triggered PowerShell processor.
- Key Vault provides the controlled location for secrets that cannot use managed identity. This demo deliberately avoids creating a fake Service Bus secret because Service Bus supports identity-based connections.
- Terraform creates, connects, documents, and removes the complete environment.
## Security choices
- Service Bus local authentication is disabled.
- Event Grid uses its system-assigned identity and the Azure Service Bus Data Sender role.
- The Function App uses its system-assigned identity and the Azure Service Bus Data Receiver role.
- The Function identity receives Key Vault Secrets User at the vault scope.
- The Event Grid access key is retrieved only for the temporary publisher test and is not written to Terraform outputs or repository files.
- Local Terraform state is excluded from Git because state can contain sensitive infrastructure values.
Why record these choices?
The responsibility section explains why each service belongs in the architecture. This helps a reader understand the design instead of seeing a list of resources.
The security section shows how managed identity uses Azure RBAC for scoped access. It also explains the temporary publisher key.
- Save README.md.
- Return to the Markdown preview.
You should see two scannable sections beneath the architecture diagram. The security bullets should state that Service Bus local authentication is disabled.
Security section in the wrong place?
- Place the new content after the closing Mermaid fence.
- Check that both section titles begin with two hash characters.
Need another pair of eyes? Help me check the heading order in my README.
- Add the reproducible deployment procedure below the security section by pasting:
## Deploy
1. Copy `terraform.tfvars.example` to `terraform.tfvars` and insert the selected subscription ID.
2. Authenticate and review the selected subscription:
```powershell
az login
az account show --output table
```
3. Initialize and validate Terraform:
```powershell
terraform init
terraform fmt
terraform validate
terraform plan
```
4. Deploy the environment:
```powershell
terraform apply
terraform output
```
Why include the deployment flow?
These commands turn the repository into a reproducible deployment guide. A reader can authenticate before reviewing the plan.
The final output step exposes the generated resource names required for the demonstration.
- Save README.md.
- Return to the Markdown preview.
You should see four numbered deployment stages with three PowerShell command blocks.
Numbering looks broken?
- Keep each PowerShell fence on a separate line.
- Leave one blank line around every fenced command block.
Still seeing malformed steps? Help me fix the Markdown numbering around my PowerShell code fences.
- Add the event demonstration and fan-out validation procedure below the deployment section by pasting:
## Demonstrate the event flow
Copy the generated resource group and Event Grid topic names from `terraform output`, then run:
```powershell
./send-event.ps1 -ResourceGroupName "<resource-group-name>" -TopicName "<event-grid-topic-name>"
```
A successful publish reports HTTP status 200. The Function consumes the queue copy. The analytics and notifications subscriptions retain independent copies that can be inspected with Service Bus Explorer in the Azure portal.
## Validate fan-out
1. Open the deployed Service Bus namespace in the Azure portal.
2. Select Topics, select `order-events`, and open Service Bus Explorer.
3. Select the `analytics` subscription, choose Peek Mode, and select Peek from start.
4. Repeat for `notifications` and compare the event bodies.
What does this demonstration prove?
The HTTP response proves that Event Grid accepted the custom event. The empty queue proves that OrderProcessor consumed its processing copy.
The two subscription checks prove that analytics and notifications retain independent copies.
- Save README.md.
- Return to the Markdown preview.
You should see the publisher command followed by a four-stage portal validation procedure.
Demonstration command missing from the preview?
- Check that the command sits between matching PowerShell fences.
- Confirm that the resource placeholders still use angle brackets.
Need help comparing the section? Help me find the formatting problem in my README demonstration section.
- Finish README.md with the design limitation and teardown warning by pasting:
## Design limitation
The project uses local Terraform state to stay focused and finish quickly. A production evolution should store state remotely, separate infrastructure deployment from Function code delivery, and use workload identity federation in CI/CD.
## Destroy
The B1 App Service plan and Service Bus Standard namespace continue to accrue charges while deployed.
```powershell
terraform destroy
```
Why end with lifecycle guidance?
The limitation shows that you understand how this focused project differs from a production workflow. The destroy section gives readers a clear way to stop billable resources.
- Save README.md.
- Return to the Markdown preview.
You should see the local-state limitation followed by a visible cost warning and the teardown command.
Final sections missing?
- Scroll below the fan-out validation list in README.md.
- Confirm that both final headings begin with two hash characters.
Need help finding the mismatch? Help me compare the final README sections with the expected design limitation and destroy warning.
✔️ Awesome, I've got everything!
Your README now explains how to understand, deploy, demonstrate, and remove the pipeline.
ⓧ I'd like to double check the full code
# Azure Event-Driven Order Pipeline with Terraform
This project provisions and demonstrates an event-driven order workflow on Microsoft Azure. Terraform owns the infrastructure and deploys a minimal PowerShell Function package.
## Architecture
```mermaid
flowchart LR
P[Order publisher] --> EG[Azure Event Grid custom topic]
EG -->|Managed identity| Q[Service Bus queue: orders]
Q --> F[PowerShell Function: OrderProcessor]
EG -->|Managed identity| T[Service Bus topic: order-events]
T --> A[Subscription: analytics]
T --> N[Subscription: notifications]
F -. Key Vault URI and RBAC .-> KV[Azure Key Vault]
```
## Why each service exists
- Event Grid accepts discrete order events and routes them to interested handlers.
- The Service Bus queue provides one reliable processing path for the order processor.
- The Service Bus topic gives independent consumers their own event copies.
- The Function App runs the queue-triggered PowerShell processor.
- Key Vault provides the controlled location for secrets that cannot use managed identity. This demo deliberately avoids creating a fake Service Bus secret because Service Bus supports identity-based connections.
- Terraform creates, connects, documents, and removes the complete environment.
## Security choices
- Service Bus local authentication is disabled.
- Event Grid uses its system-assigned identity and the Azure Service Bus Data Sender role.
- The Function App uses its system-assigned identity and the Azure Service Bus Data Receiver role.
- The Function identity receives Key Vault Secrets User at the vault scope.
- The Event Grid access key is retrieved only for the temporary publisher test and is not written to Terraform outputs or repository files.
- Local Terraform state is excluded from Git because state can contain sensitive infrastructure values.
## Deploy
1. Copy `terraform.tfvars.example` to `terraform.tfvars` and insert the selected subscription ID.
2. Authenticate and review the selected subscription:
```powershell
az login
az account show --output table
```
3. Initialize and validate Terraform:
```powershell
terraform init
terraform fmt
terraform validate
terraform plan
```
4. Deploy the environment:
```powershell
terraform apply
terraform output
```
## Demonstrate the event flow
Copy the generated resource group and Event Grid topic names from `terraform output`, then run:
```powershell
./send-event.ps1 -ResourceGroupName "<resource-group-name>" -TopicName "<event-grid-topic-name>"
```
A successful publish reports HTTP status 200. The Function consumes the queue copy. The analytics and notifications subscriptions retain independent copies that can be inspected with Service Bus Explorer in the Azure portal.
## Validate fan-out
1. Open the deployed Service Bus namespace in the Azure portal.
2. Select Topics, select `order-events`, and open Service Bus Explorer.
3. Select the `analytics` subscription, choose Peek Mode, and select Peek from start.
4. Repeat for `notifications` and compare the event bodies.
## Design limitation
The project uses local Terraform state to stay focused and finish quickly. A production evolution should store state remotely, separate infrastructure deployment from Function code delivery, and use workload identity federation in CI/CD.
## Destroy
The B1 App Service plan and Service Bus Standard namespace continue to accrue charges while deployed.
```powershell
terraform destroy
```
How to use this reference
Compare this full file with your saved README.md. Each section documents one part of the architecture or its lifecycle.
Confirm repository exclusions
A public repository should contain the reusable configuration without generated packages or local values. A .gitignore file tells Git which local artifacts to leave out.
- Select .gitignore from the VS Code file list.
- Compare its contents with this protected-file list:
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
terraform.tfvars
function.zip
crash.log
What stays out of Git?
- Terraform state can contain sensitive infrastructure values.
- The terraform.tfvars file contains deployment-specific input values.
- The function.zip package is generated from the Function source files.
- Plan files and crash logs are local execution artifacts.
You should see all seven exclusion patterns in your existing file. This keeps the reusable source separate from local deployment artifacts.
Exclusion missing from the file?
- Compare each line with the reference above.
- Remove any leading spaces before the exclusion patterns.
Need help checking the file? Help me compare my .gitignore file with the required Terraform exclusions.
Prove the deployment is unchanged
Documentation becomes credible when another person can reproduce its claims. Your final evidence combines the message copies in Service Bus Explorer with a clean infrastructure plan.
- Return to Service Bus Explorer in the Azure portal from the previous step.
- Select the analytics subscription.
- Capture a screenshot showing its retained order event in Peek Mode.
- Select the notifications subscription.
- Capture a screenshot showing its independently retained order event in Peek Mode.
- Save all project files in VS Code.
These screenshots show that the two subscribers received separate copies. Keep access keys and local variable values outside every screenshot.
- Format the Terraform configuration by running:
terraform fmt
What does this command check?
Terraform formats the configuration into its standard layout. The command returns control to PowerShell when formatting is complete.
You should return to the PowerShell prompt without a formatting error.
- Validate the initialized Terraform configuration by running:
terraform validate
What does validation prove?
Validation checks the configuration syntax and its internal consistency. It catches structural problems before Terraform compares the code with Azure.
You should see a success confirmation for the configuration.
Validation did not succeed?
- Save every .tf file in VS Code.
- Review the file and line identified by Terraform.
Still blocked? Help me troubleshoot my Terraform validation result.
Before you run the final check, decide whether you expect Terraform to propose an infrastructure change.
- Compare the saved configuration with the deployed resources by running:
terraform plan
What does the final plan prove?
Terraform compares the saved configuration with the deployed Azure resources. You should see a summary reporting no infrastructure changes.
A clean plan proves that the repository documents the same system you tested.
Strong finish. Your repository now explains the architecture and proves that the live deployment matches its Terraform configuration.
Plan proposes unexpected changes?
- Review each proposed change before running another apply.
- Check that every .tf file is saved.
- Confirm that PowerShell still uses the selected Azure subscription from earlier.
Need help reading the plan? Help me understand why my final Terraform plan proposes changes.
Secret mission
Route Only Priority Events to Fan-Out
Your fan-out route currently gives every order to both independent subscribers. In this mission, you will add subject-prefix filtering so analytics and notifications retain priority orders while the processing queue continues to handle every order.
Clean Up Your Resources
Clean Up Your Resources
Your deployed B1 App Service plan and Service Bus Standard namespace continue to accrue charges. Decide whether to keep the live demo, pause Function processing, or delete the environment entirely.
Cost warning
Leaving the deployment active can consume your remaining Azure credit. The B1 App Service plan and Service Bus Standard namespace carry ongoing charges.
Operations involving Key Vault, Event Grid, storage, or messaging can also add usage charges. Choose the delete option when your live demo is finished.
Resources you used:
- One Terraform-managed Azure resource group.
- One storage account for the Function host.
- One B1 Linux service plan.
- One Linux Function App hosting OrderProcessor.
- One RBAC-enabled Key Vault.
- One Service Bus Standard namespace.
- One Service Bus queue named orders.
- One Service Bus topic named order-events.
- One topic subscription named analytics.
- One topic subscription named notifications.
- One Event Grid custom topic.
- One Event Grid event subscription named orders-to-queue.
- One Event Grid event subscription named orders-to-topic with priority subject filtering.
- Scoped Azure RBAC role assignments for messaging access.
- Your local project folder from Step 1. It contains function.zip plus sensitive Terraform state.
Keep everything running
No action is needed. Choose this option if you want to preserve the live pipeline for demonstrations or further development.
- Leave the Terraform-managed resource group deployed.
- Keep the local project folder on your workstation.
- Check your subscription credit balance regularly to confirm how much promotional credit remains.
- Keep .gitignore in place so Terraform state, variable values, generated plans, and function.zip stay out of source control.
Pause - I'll come back to this later
Stopping the Function App pauses order processing while preserving the deployed environment. The B1 service plan and Service Bus Standard namespace remain billable during this pause.
- Return to the Function App page from earlier in the Azure portal.
- Select Stop in the page toolbar.
- Confirm the stop request if Azure asks for confirmation.
- Check the Function App page until it shows that processing has stopped.
- Keep the local project folder so Terraform can manage the same environment when you return.
Your Function processing is paused, so the existing environment remains available for a later restart.
Delete - I don't want to use this again
Remove the Azure deployment before deleting your local files. Terraform needs its local state to identify the resources it manages.
Destroy the Cloud Resources First
Deleting the resource group looks broad because it removes every nested Azure resource in one operation. Terraform shows the destruction plan first, so you can review the scope before confirming.
Key Vault remains soft-deleted for seven days after the resource group is removed. Its generated name stays temporarily reserved.
- Return to the project folder from earlier in Windows PowerShell.
- Start the Terraform cleanup by running this command:
terraform destroy
What Does This Command Do?
Terraform reads local state to identify every managed Azure resource. It presents a destruction plan before making changes.
After you confirm, Terraform removes the resource group with every nested resource. Your local project folder stays on disk for the final cleanup action.
- Review the destruction plan to confirm it covers the project resources.
- Type yes when Terraform asks for confirmation.
- Wait until Terraform reports that destruction has completed.
- Return to the Azure portal page from earlier.
- Confirm the Terraform-generated resource group no longer appears.
- Use your Windows file browser to locate the project folder from Step 1.
- Delete the project folder.
- Confirm the project folder no longer appears in its parent folder.
That closes the billable Azure environment and removes sensitive local state from this workstation.
Nice Work!
Nice Work!
You did it! You built a documented event-driven order pipeline on Microsoft Azure with Terraform.
You've learned how to:
- Provisioned a repeatable messaging environment that routes order events from Azure Event Grid to Azure Service Bus. The queue feeds one processor. The topic preserves one copy for analytics. It preserves another copy for notifications.
- Packaged OrderProcessor as a PowerShell Function on Azure Functions. Protected its Service Bus access with a managed identity plus Azure RBAC. Authorized the same identity for Key Vault.
- Documented the deployment in README.md with a Mermaid architecture diagram. Confirmed the final Terraform plan showed no infrastructure changes. Captured a safe teardown path for billable resources.
- Secret Mission: Added a case-insensitive subject-prefix filter to orders-to-topic. Priority orders now reach both topic subscriptions. Standard orders remain on the queue-only path.
Ready to quiz yourself?