Multi-stage Terraform pattern

Terraform Sandwich

Jon Abbott

Terraform

What is Terraform?

Infrastructure as code: declare cloud and SaaS resources in versioned files, then plan and apply changes through one consistent workflow.

  • Configuration describes the desired state
  • State tracks what already exists
  • Providers translate that into API calls
Terraform uses a provider to call a target API
Write, plan, and apply workflow

Why use Terraform?

  • Manage any infrastructure
  • Track your infrastructure
  • Automate changes
  • Standardise configurations
  • Collaborate

When love and hate collide

Terraform Sandwich

A multi-stage Terraform pattern: run Terraform more than once in a CI/CD pipeline, with an application release between the stages.

  • Stage 1 creates everything safe to provision first
  • Release deploys and starts the application
  • Stage 2 creates resources that need the app to be alive

For example: Azure Event Grid

Event Grid validates the Azure Function endpoint when the subscription is created — function code must already exist, not just the Function App.

  • You cannot create the subscription until the function (with its name and metadata) is deployed
  • You cannot deploy that function until the Function App is provisioned
  • A single greenfield apply hits a circular dependency

What fails on first apply

Azure rejects the subscription while polling CreateOrUpdate — Terraform exits with code 1.

  • Code: Endpoint validation
  • Message gist: destination endpoint not found — the function resource should pre-exist
  • Typical path ends in …/functions/<FunctionName>

Solution

  • Terraform 1: Function App + topic; subscriptions disabled (eventgrid_subscriptions_enabled = false)
  • Release: deploy the named function so it is discoverable
  • Terraform 2: enable the flag and create the subscriptions
CI pipeline with two Terraform stages around a software release

Gate with count

Conditionally include the subscription until the function endpoint can pass validation.


resource "azurerm_eventgrid_event_subscription" "internal" {
  count = var.eventgrid_subscriptions_enabled ? 1 : 0

  name  = "evgs-${var.name}-internal"
  scope = azurerm_eventgrid_topic.main.id

  azure_function_endpoint {
    function_id = local.subscriber_endpoint
  }
}
            

When to use it

  • Use when create depends on deployed app behaviour or a live endpoint
  • Mostly a first-bring-up problem — later deploys are one-pass if the function name stays stable
  • Keep shared state clear between the two Terraform stages
  • Avoid it when a single apply (or a provisioner-free redesign) is enough

Thank you

Terraform Sandwich